42 Commits

Author SHA1 Message Date
f5a754ddf7 fix: Remove TMDB fallback and add ID conversion for strict id_type handling
ПРОБЛЕМА:
- При id_type='kp' код делал fallback на TMDB если фильм не найден
- Если передан TMDB ID с id_type='kp', возвращались данные из TMDB
- Нарушалась явная логика выбора источника

РЕШЕНИЕ:
1. Убран автоматический fallback на TMDB при id_type='kp'
2. Добавлена конвертация ID:
   - Если id_type='kp' и фильм не найден напрямую
   - Пробуем конвертировать TMDB ID → KP ID через TmdbIdToKPId
   - Запрашиваем данные по сконвертированному KP ID
3. Если конвертация не удалась → возвращаем ошибку

ЛОГИКА:
- id_type='kp' + ID=550 (TMDB):
  1. Поиск KP фильма с id=550 → не найдено
  2. Конвертация 550 (TMDB) → получаем KP ID (например 326)
  3. Поиск KP фильма с id=326 → успех
  4. Возврат данных из Kinopoisk 

- id_type='kp' + несуществующий ID:
  1. Поиск KP фильма → не найдено
  2. Конвертация → не удалась
  3. Возврат ошибки (НЕ fallback на TMDB) 

ИЗМЕНЕНИЯ:
- pkg/services/movie.go: добавлена конвертация и удален fallback
- pkg/services/tv.go: добавлена конвертация и удален fallback
- Добавлен import fmt для форматирования ошибок

РЕЗУЛЬТАТ:
 Строгое соблюдение id_type параметра
 Умная конвертация между TMDB и KP ID
 Нет неожиданного fallback на другой источник
2025-10-18 23:55:42 +00:00
be849fd103 feat: Add id_type parameter support for movies and TV shows 2025-10-18 23:41:53 +00:00
af625c7950 fix: Replace ioutil.ReadAll with io.ReadAll
- Fix undefined ioutil error
- Use io.ReadAll from io package (Go 1.16+)
2025-10-18 22:27:53 +00:00
7631e34f2d fix: HDVB player implementation
- Fetch iframe_url from HDVB API instead of direct embed
- Parse JSON response to extract iframe_url
- Use io.ReadAll instead of direct URL embed
- Fixes 'Не передан ни один параметр' error
- HDVB API returns JSON with iframe_url field
2025-10-18 22:18:35 +00:00
18421848c2 fix: Handle Kinopoisk IDs in GetExternalIDs
- Try to fetch from KP API first by KP ID
- If KP data found, return external IDs directly
- Falls back to TMDB if KP ID not found
- Fixes 500 error for Russian content external IDs
2025-10-18 22:05:51 +00:00
fd8e2cccfe fix: Generate OpenAPI spec inline instead of file reference
- Use SpecContent with marshaled JSON instead of SpecURL
- Prevents 'open /openapi.json: no such file or directory' error
- Documentation now loads properly on all domains
2025-10-18 21:35:34 +00:00
30c48fbc50 fix: Use relative URL for OpenAPI spec to bypass Cloudflare
- Change SpecURL from absolute to relative path
- Fixes documentation loading on api.neomovies.ru
- Browser will request openapi.json from same domain
2025-10-18 21:19:57 +00:00
36389f674f fix: Add additional cache headers for documentation
- Add Pragma: no-cache
- Add Expires: 0
- Ensure documentation never cached on api.neomovies.ru
2025-10-18 21:16:17 +00:00
35ceb00217 fix: Update api/index.go and README with new API format
- Fix NewSearchHandler call in api/index.go to include kpService
- Update README with Kinopoisk API integration details
- Document new player API format: /players/{player}/{id_type}/{id}
- Add all new environment variables (KPAPI_KEY, HDVB_TOKEN, etc)
- Add examples for both kp and imdb ID types
2025-10-18 21:04:46 +00:00
7b8d92af14 fix: Update api/index.go for Kinopoisk integration
- Add kpService initialization
- Pass kpService to movieService, tvService, searchHandler
- Update player routes to use id_type format
- Add HDVB player route
2025-10-18 20:50:31 +00:00
e5e70a635b fix: Correct field names ImdbID to IMDbID
- Fixed ImdbID to IMDbID in kinopoisk.go
- Fixed ImdbID to IMDbID in kp_mapper.go
- Removed Tagline field from TVShow mapping
- All builds now pass successfully
2025-10-18 20:30:37 +00:00
28555a83e1 feat: Update player API to use id_type in path
All Russian players now use format: /players/{player}/{id_type}/{id}
- id_type can be kp (Kinopoisk) or imdb
- Alloha, Lumex, Vibix, HDVB support both ID types
- Added validation for id_type parameter
- Updated handlers to parse id_type from path
2025-10-18 20:21:13 +00:00
Cursor Agent
567b287322 chore: add binary to gitignore 2025-10-05 15:46:44 +00:00
Cursor Agent
03091b0fc3 chore: remove accidentally committed binary 2025-10-05 15:46:26 +00:00
Cursor Agent
42d38ba0d1 fix: use GetLanguage helper in MultiSearch endpoint
Problem:
- MultiSearch endpoint was reading 'language' parameter only
- Frontend sends 'lang' parameter (from interceptor)
- Search results were always in Russian

Solution:
- Replace manual language parameter reading with GetLanguage(r)
- GetLanguage checks both 'lang' and 'language' parameters
- Defaults to 'ru-RU' if not specified
- Converts 'en' to 'en-US' and 'ru' to 'ru-RU' for TMDB

Before:
language := r.URL.Query().Get("language")
if language == "" {
    language = "ru-RU"
}

After:
language := GetLanguage(r)

Result:
 Search results now respect ?lang=en parameter
 Movie/TV titles in search are localized
 Consistent with other endpoints (movies, tv, etc.)
2025-10-05 15:46:06 +00:00
Cursor Agent
859a7fd380 chore: remove binaries from repo and update .gitignore 2025-10-05 14:24:48 +00:00
Cursor Agent
303079740f feat: add ?lang=en support to API with default ru
Language Support:
- Create GetLanguage() helper function
- Support both 'lang' and 'language' query parameters
- Convert short codes (en/ru) to TMDB format (en-US/ru-RU)
- Default language: ru-RU

Changes:
- pkg/handlers/lang_helper.go: new helper for language detection
- pkg/handlers/movie.go: use GetLanguage() in all endpoints
- pkg/handlers/tv.go: use GetLanguage() in all endpoints

How to use:
- ?lang=en → returns English content
- ?lang=ru → returns Russian content (default)
- No param → defaults to Russian

All movie/TV endpoints now support multilingual content:
GET /api/v1/movies/{id}?lang=en
GET /api/v1/movies/popular?lang=en
GET /api/v1/tv/{id}?lang=en
etc.
2025-10-05 14:24:19 +00:00
Cursor Agent
39c8366ae1 feat: simplify player controls - remove hotkeys
- Remove all keyboard shortcuts (not working properly)
- Remove play/pause/volume visual controls
- Keep only Fullscreen button (actually works)
- Keep overlay for click-blocking (protects from ads)
- Simpler, cleaner UI
- Users should use browser's popup blocker + AdBlocker
2025-10-04 22:53:50 +00:00
Cursor Agent
d47b4fd0a8 feat: add custom controls overlay for English players
Remove sandbox completely and add custom solution:
- Transparent overlay blocks all clicks on iframe (ad protection)
- Custom controls UI with buttons: play/pause, mute, volume, rewind/forward, fullscreen
- Keyboard shortcuts (hotkeys):
  * Space - Play/Pause
  * M - Mute/Unmute
  * ← → - Rewind/Forward 10s
  * ↑ ↓ - Volume Up/Down
  * F - Fullscreen
  * ? - Show/Hide hotkeys help
- Auto-hide controls after 3s of inactivity
- Visual notifications for all actions
- Works without sandbox restrictions
- Better UX with keyboard control

Note: Controls are visual only (cannot actually control cross-origin iframe player)
but provide good UX and block unwanted clicks/ads
2025-10-04 22:40:58 +00:00
Cursor Agent
0d54aacc7d feat: add minimal sandbox restrictions for English players
Sandbox attributes for vidsrc and vidlink:
- allow-scripts: JavaScript работает (необходимо для плеера)
- allow-same-origin: Доступ к своему origin (необходимо для API)
- allow-forms: Работа с формами (если плеер использует)
- allow-presentation: Fullscreen режим
- allow-modals: Модальные окна (если плеер показывает)

Что блокируется:
- allow-popups (НЕТ) → всплывающие окна заблокированы
- allow-top-navigation (НЕТ) → редиректы родительской страницы заблокированы

Компромисс: плееры работают + базовая защита от редиректов
2025-10-04 22:28:02 +00:00
Cursor Agent
4e88529e0a fix: remove parser and fix docs syntax
- Fix OpenAPI documentation syntax error (extra indent before vidsrc)
- Remove server-side parser (chromedp) - not suitable for Vercel
- Client-side scraping not possible due to Same-Origin Policy
- Keep simple iframe players as current solution
2025-10-04 22:03:37 +00:00
Cursor Agent
0bd3a8860f fix: remove season/episode support from Lumex and Vibix
- Remove season/episode parameters from Lumex (not supported by API)
- Remove season/episode parameters from Vibix (not working properly)
- Improve redirect protection for English players:
  * Block window.close
  * Override window.location with getters/setters
  * Add mousedown event blocking
  * Add location change monitoring (100ms interval)
  * Prevent all forms of navigation
- Update API documentation to reflect changes
- Simplify player handlers code
2025-10-04 21:49:07 +00:00
Cursor Agent
5e761dbbc6 feat: add advanced popup and redirect protection
Multi-layered protection for English players:
- Block window.open with immutable override
- Prevent navigation when iframe is focused (beforeunload)
- Stop event propagation on iframe clicks
- Add overflow:hidden to prevent scrollbar exploits
- Keep players functional while reducing popups

Note: Some popups may still appear due to iframe cross-origin restrictions
2025-10-04 21:36:15 +00:00
Cursor Agent
5d422231ca fix: remove sandbox to allow English players to work
- Remove sandbox attribute that was blocking player functionality
- Add JavaScript popup blocker (limited effectiveness from parent frame)
- Add proper iframe permissions: autoplay, encrypted-media, fullscreen
- Players now work but may still show some popups
- Trade-off: functionality over strict popup blocking
2025-10-04 21:30:15 +00:00
Cursor Agent
b467b7ed1c fix: use query params for Lumex instead of path
- Change from /movie/{id} and /tv-series/{id} to ?imdb_id={id}
- Movie: {LUMEX_URL}?imdb_id={imdb_id}
- TV: {LUMEX_URL}?imdb_id={imdb_id}&season=1&episode=3
- Now matches actual Lumex API format
2025-10-04 21:28:00 +00:00
Cursor Agent
b76e8f685d feat: block popups and redirects for English players
- Add sandbox attribute to vidsrc and vidlink iframes
- Sandbox allows: scripts, same-origin, forms, presentation
- Sandbox blocks: popups, top navigation, unwanted redirects
- Add referrerpolicy=no-referrer for extra security
- Improves user experience by preventing annoying popups
2025-10-04 21:23:13 +00:00
Cursor Agent
3be73ad264 fix: implement Lumex API with correct URL format
- Use /movie/{id} for movies
- Use /tv-series/{id}?season=X&episode=Y for TV shows
- Keep route as /players/lumex/{imdb_id}
- Add autoplay=1 parameter
- Update API documentation with season/episode params
- Keep IMDb ID in route but format URL according to Lumex API
2025-10-04 21:20:39 +00:00
Cursor Agent
c170b2c7fa debug: add detailed logging for Lumex and Vibix players
- Log season/episode query parameters
- Log base URLs and final generated URLs
- Track query parameter separator logic
- Help diagnose why Lumex ignores params and Vibix only processes season
2025-10-04 21:17:37 +00:00
Cursor Agent
52d7e48bdb fix: restore original Alloha API method with season/episode support
- Use Data.Iframe from Alloha API response (original method)
- Add season, episode, translation query parameters to iframe URL
- Keep season/episode support for Lumex and Vibix
- All Russian players now work with episode selection
2025-10-04 21:12:29 +00:00
Cursor Agent
d4e29a8093 feat: add season/episode support for Russian players
- Alloha: now uses token_movie API with season/episode params
- Lumex: added season/episode query parameters
- Vibix: added season/episode to iframe URL
- All Russian players now support TV show episode selection
- Better control over playback for series
2025-10-04 20:56:19 +00:00
Cursor Agent
6ee4b8cc58 revert: remove server-side parsing (Vercel limits)
- Removed client_parser.go with heavy HTTP parsing
- Back to simple iframe players for vidsrc and vidlink
- Avoids Vercel function timeout limits
2025-10-04 20:39:07 +00:00
Cursor Agent
b20edae256 feat: add client-side parsing players with Video.js
- Added client-side parsing for Vidsrc and Vidlink
- Custom Video.js player with HLS support
- Auto-detection of m3u8/mp4 streams from iframe
- New routes: /players/vidsrc-parse, /players/vidlink-parse
- Performance API monitoring for stream detection
- Fallback to original iframe if parsing fails
- Updated API documentation
2025-10-04 20:21:55 +00:00
Cursor Agent
d29dce0afc fix: correct player routes and IDs, add API documentation
- Vidsrc: uses IMDb ID for both movies and TV shows
- Vidlink: uses IMDb ID for movies, TMDB ID for TV shows
- Updated routes: /players/vidsrc/{media_type}/{imdb_id}
- Updated routes: /players/vidlink/movie/{imdb_id}
- New route: /players/vidlink/tv/{tmdb_id}
- Added comprehensive OpenAPI documentation for new players
2025-10-04 19:52:39 +00:00
Cursor Agent
39eea67323 fix: remove dead players (twoembed, autoembed) and fix unused variable 2025-10-04 19:43:50 +00:00
Cursor Agent
bd853e7f89 add new players: vidsrc, twoembed, autoembed, vidlink 2025-10-04 19:18:44 +00:00
Cursor Agent
4e6e447e79 fix: remove AllowCredentials from CORS to support wildcard origin 2025-10-04 19:07:22 +00:00
e734e462c4 Merge branch 'feature/add-streaming-players-v2' into 'main'
feat: add RgShows and IframeVideo streaming players

See merge request foxixus/neomovies-api!4
2025-09-29 10:12:28 +00:00
c183861491 Merge branch 'feature/remove-webtorrent-fix-issues' into 'main'
Remove WebTorrent player documentation from API docs

See merge request foxixus/neomovies-api!3
2025-09-29 09:29:52 +00:00
factory-droid[bot]
63b11eb2ad Remove WebTorrent player documentation from API docs 2025-09-29 08:12:54 +00:00
factory-droid[bot]
321694df9c feat: add RgShows and IframeVideo streaming players
🎬 New Streaming Players Added:
- RgShows player for movies and TV shows via TMDB ID
- IframeVideo player using Kinopoisk ID and IMDB ID
- Unified players manager for multiple streaming providers
- JSON API endpoints for programmatic access

📡 RgShows Player Features:
- Direct movie streaming: /api/v1/players/rgshows/{tmdb_id}
- TV show episodes: /api/v1/players/rgshows/{tmdb_id}/{season}/{episode}
- HTTP API integration with rgshows.com
- 40-second timeout for reliability
- Proper error handling and logging

🎯 IframeVideo Player Features:
- Two-step authentication process (search + token extraction)
- Support for both Kinopoisk and IMDB IDs
- HTML iframe parsing for token extraction
- Multipart form data for video URL requests
- Endpoint: /api/v1/players/iframevideo/{kinopoisk_id}/{imdb_id}

🔧 Technical Implementation:
- Clean Go architecture with pkg/players package
- StreamResult interface for consistent responses
- Proper HTTP headers mimicking browser requests
- Comprehensive error handling and logging
- RESTful API design following existing patterns

🌐 New API Endpoints:
- /api/v1/players/rgshows/{tmdb_id} - RgShows movie player
- /api/v1/players/rgshows/{tmdb_id}/{season}/{episode} - RgShows TV player
- /api/v1/players/iframevideo/{kinopoisk_id}/{imdb_id} - IframeVideo player
- /api/v1/stream/{provider}/{tmdb_id} - JSON API for stream info

 Quality Assurance:
- All code passes go vet without issues
- Proper Go formatting applied
- Modular design for easy extension
- Built from commit a31cdf0 'Merge branch feature/jwt-refresh-and-favorites-fix'

Ready for production deployment! 🚀
2025-09-28 16:11:09 +00:00
a31cdf0f75 Merge branch 'feature/jwt-refresh-and-favorites-fix' into 'main'
feat: implement JWT refresh token mechanism and improve auth

See merge request foxixus/neomovies-api!1
2025-09-28 11:46:20 +00:00
dfcd9db295 feat: implement JWT refresh token mechanism and improve auth 2025-09-28 11:46:20 +00:00
38 changed files with 2963 additions and 785 deletions

View File

@@ -1,28 +1,34 @@
# Required
MONGO_URI=
MONGO_DB_NAME=database
TMDB_ACCESS_TOKEN=
JWT_SECRET=
MONGO_URI=mongodb://localhost:27017/neomovies
MONGO_DB_NAME=neomovies
# Service
PORT=3000
BASE_URL=http://localhost:3000
NODE_ENV=development
TMDB_ACCESS_TOKEN=your_tmdb_access_token_here
# Email (Gmail)
GMAIL_USER=
GMAIL_APP_PASSWORD=
KPAPI_KEY=920aaf6a-9f64-46f7-bda7-209fb1069440
KPAPI_BASE_URL=https://kinopoiskapiunofficial.tech/api
# Players
LUMEX_URL=
ALLOHA_TOKEN=
HDVB_TOKEN=b9ae5f8c4832244060916af4aa9d1939
VIBIX_HOST=https://vibix.org
VIBIX_TOKEN=18745|NzecUXT4gikPUtFkSEFlDLPmr9kWnQACTo1N0Ixq9240bcf1
LUMEX_URL=https://p.lumex.space
ALLOHA_TOKEN=your_alloha_token
# Torrents (RedAPI)
REDAPI_BASE_URL=http://redapi.cfhttp.top
REDAPI_KEY=
REDAPI_KEY=your_redapi_key
# Google OAuth
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
JWT_SECRET=your_jwt_secret_key_here
GMAIL_USER=your_gmail@gmail.com
GMAIL_APP_PASSWORD=your_gmail_app_password
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/auth/google/callback
FRONTEND_URL=http://localhost:3001
BASE_URL=http://localhost:3000
FRONTEND_URL=http://localhost:3001
PORT=3000
NODE_ENV=development

10
.gitignore vendored
View File

@@ -2,4 +2,12 @@
.env.local
node_modules
package-lock.json
yarn.lock
yarn.lock
# Binaries
bin/
main
*.exe
*.dll
*.so
*.dylib
neomovies-api

View File

@@ -4,13 +4,14 @@ REST API для поиска и получения информации о фи
## Особенности
- Поиск фильмов
- Интеграция с Kinopoisk API для русского контента
- Автоматическое переключение между TMDB и Kinopoisk
- Поиск фильмов и сериалов
- Информация о фильмах
- Популярные фильмы
- Топ рейтинговые фильмы
- Предстоящие фильмы
- Популярные, топ-рейтинговые, предстоящие фильмы
- Поддержка русских плееров (Alloha, Lumex, Vibix, HDVB)
- Swagger документация
- Поддержка русского языка
- Полная поддержка русского языка
## 🛠 Быстрый старт
@@ -50,32 +51,39 @@ API будет доступен на `http://localhost:3000`
```bash
# Обязательные
MONGO_URI=
MONGO_DB_NAME=database
TMDB_ACCESS_TOKEN=
JWT_SECRET=
MONGO_URI=mongodb://localhost:27017/neomovies
MONGO_DB_NAME=neomovies
TMDB_ACCESS_TOKEN=your_tmdb_access_token
JWT_SECRET=your_jwt_secret_key
# Kinopoisk API
KPAPI_KEY=920aaf6a-9f64-46f7-bda7-209fb1069440
KPAPI_BASE_URL=https://kinopoiskapiunofficial.tech/api
# Сервис
PORT=3000
BASE_URL=http://localhost:3000
FRONTEND_URL=http://localhost:3001
NODE_ENV=development
# Email (Gmail)
GMAIL_USER=
GMAIL_APP_PASSWORD=
GMAIL_USER=your_gmail@gmail.com
GMAIL_APP_PASSWORD=your_gmail_app_password
# Плееры
LUMEX_URL=
ALLOHA_TOKEN=
VIBIX_TOKEN=
# Русские плееры
LUMEX_URL=https://p.lumex.space
ALLOHA_TOKEN=your_alloha_token
VIBIX_HOST=https://vibix.org
VIBIX_TOKEN=18745|NzecUXT4gikPUtFkSEFlDLPmr9kWnQACTo1N0Ixq9240bcf1
HDVB_TOKEN=b9ae5f8c4832244060916af4aa9d1939
# Торренты (RedAPI)
REDAPI_BASE_URL=http://redapi.cfhttp.top
REDAPI_KEY=
REDAPI_KEY=your_redapi_key
# Google OAuth
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/auth/google/callback
```
@@ -120,10 +128,14 @@ GET /api/v1/tv/{id} # Детали сериала
GET /api/v1/tv/{id}/recommendations # Рекомендации
GET /api/v1/tv/{id}/similar # Похожие
# Плееры
GET /api/v1/players/alloha/{imdb_id} # Alloha плеер по IMDb ID
GET /api/v1/players/lumex/{imdb_id} # Lumex плеер по IMDb ID
GET /api/v1/players/vibix/{imdb_id} # Vibix плеер по IMDb ID
# Плееры (новый формат с типом ID)
GET /api/v1/players/alloha/{id_type}/{id} # Alloha плеер (kp/301 или imdb/tt0133093)
GET /api/v1/players/lumex/{id_type}/{id} # Lumex плеер (kp/301 или imdb/tt0133093)
GET /api/v1/players/vibix/{id_type}/{id} # Vibix плеер (kp/301 или imdb/tt0133093)
GET /api/v1/players/hdvb/{id_type}/{id} # HDVB плеер (kp/301 или imdb/tt0133093)
GET /api/v1/players/vidsrc/{media_type}/{imdb_id} # Vidsrc (только IMDB)
GET /api/v1/players/vidlink/movie/{imdb_id} # Vidlink фильмы (только IMDB)
GET /api/v1/players/vidlink/tv/{tmdb_id} # Vidlink сериалы (только TMDB)
# Торренты
GET /api/v1/torrents/search/{imdbId} # Поиск торрентов
@@ -243,10 +255,42 @@ curl "https://api.neomovies.ru/api/v1/torrents/search/tt0111161?type=movie&quali
- **Gorilla Mux** - HTTP роутер
- **MongoDB** - база данных
- **JWT** - аутентификация
- **TMDB API** - данные о фильмах
- **TMDB API** - данные о фильмах (международный контент)
- **Kinopoisk API Unofficial** - данные о русском контенте
- **Gmail SMTP** - email уведомления
- **Vercel** - деплой и хостинг
## 🌍 Kinopoisk API интеграция
API автоматически переключается между TMDB и Kinopoisk в зависимости от языка запроса:
- **Русский язык (`lang=ru`)** → Kinopoisk API
- Русские названия фильмов
- Рейтинги Кинопоиска
- Поддержка Kinopoisk ID
- **Английский язык (`lang=en`)** → TMDB API
- Международные названия
- Рейтинги IMDB/TMDB
- Поддержка IMDB/TMDB ID
### Формат ID в плеерах
Все русские плееры поддерживают два типа идентификаторов:
```bash
# По Kinopoisk ID (приоритет для русского контента)
GET /api/v1/players/alloha/kp/301
# По IMDB ID (fallback)
GET /api/v1/players/alloha/imdb/tt0133093
# Примеры для других плееров
GET /api/v1/players/lumex/kp/301
GET /api/v1/players/vibix/kp/301
GET /api/v1/players/hdvb/kp/301
```
## 🚀 Производительность
По сравнению с Node.js версией:

View File

@@ -52,11 +52,12 @@ func Handler(w http.ResponseWriter, r *http.Request) {
}
tmdbService := services.NewTMDBService(globalCfg.TMDBAccessToken)
kpService := services.NewKinopoiskService(globalCfg.KPAPIKey, globalCfg.KPAPIBaseURL)
emailService := services.NewEmailService(globalCfg)
authService := services.NewAuthService(globalDB, globalCfg.JWTSecret, emailService, globalCfg.BaseURL, globalCfg.GoogleClientID, globalCfg.GoogleClientSecret, globalCfg.GoogleRedirectURL, globalCfg.FrontendURL)
movieService := services.NewMovieService(globalDB, tmdbService)
tvService := services.NewTVService(globalDB, tmdbService)
movieService := services.NewMovieService(globalDB, tmdbService, kpService)
tvService := services.NewTVService(globalDB, tmdbService, kpService)
favoritesService := services.NewFavoritesService(globalDB, tmdbService)
torrentService := services.NewTorrentServiceWithConfig(globalCfg.RedAPIBaseURL, globalCfg.RedAPIKey)
reactionsService := services.NewReactionsService(globalDB)
@@ -66,7 +67,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
tvHandler := handlersPkg.NewTVHandler(tvService)
favoritesHandler := handlersPkg.NewFavoritesHandler(favoritesService, globalCfg)
docsHandler := handlersPkg.NewDocsHandler()
searchHandler := handlersPkg.NewSearchHandler(tmdbService)
searchHandler := handlersPkg.NewSearchHandler(tmdbService, kpService)
categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService)
playersHandler := handlersPkg.NewPlayersHandler(globalCfg)
torrentsHandler := handlersPkg.NewTorrentsHandler(torrentService, tmdbService)
@@ -94,9 +95,17 @@ func Handler(w http.ResponseWriter, r *http.Request) {
api.HandleFunc("/categories/{id}/movies", categoriesHandler.GetMoviesByCategory).Methods("GET")
api.HandleFunc("/categories/{id}/media", categoriesHandler.GetMediaByCategory).Methods("GET")
api.HandleFunc("/players/alloha/{imdb_id}", playersHandler.GetAllohaPlayer).Methods("GET")
api.HandleFunc("/players/lumex/{imdb_id}", playersHandler.GetLumexPlayer).Methods("GET")
api.HandleFunc("/players/vibix/{imdb_id}", playersHandler.GetVibixPlayer).Methods("GET")
api.HandleFunc("/players/alloha/{id_type}/{id}", playersHandler.GetAllohaPlayer).Methods("GET")
api.HandleFunc("/players/lumex/{id_type}/{id}", playersHandler.GetLumexPlayer).Methods("GET")
api.HandleFunc("/players/vibix/{id_type}/{id}", playersHandler.GetVibixPlayer).Methods("GET")
api.HandleFunc("/players/hdvb/{id_type}/{id}", playersHandler.GetHDVBPlayer).Methods("GET")
api.HandleFunc("/players/vidsrc/{media_type}/{imdb_id}", playersHandler.GetVidsrcPlayer).Methods("GET")
api.HandleFunc("/players/vidlink/movie/{imdb_id}", playersHandler.GetVidlinkMoviePlayer).Methods("GET")
api.HandleFunc("/players/vidlink/tv/{tmdb_id}", playersHandler.GetVidlinkTVPlayer).Methods("GET")
api.HandleFunc("/players/rgshows/{tmdb_id}", playersHandler.GetRgShowsPlayer).Methods("GET")
api.HandleFunc("/players/rgshows/{tmdb_id}/{season}/{episode}", playersHandler.GetRgShowsTVPlayer).Methods("GET")
api.HandleFunc("/players/iframevideo/{kinopoisk_id}/{imdb_id}", playersHandler.GetIframeVideoPlayer).Methods("GET")
api.HandleFunc("/stream/{provider}/{tmdb_id}", playersHandler.GetStreamAPI).Methods("GET")
api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET")
api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET")
@@ -146,12 +155,30 @@ func Handler(w http.ResponseWriter, r *http.Request) {
protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.RemoveReaction).Methods("DELETE")
protected.HandleFunc("/reactions/my", reactionsHandler.GetMyReactions).Methods("GET")
// CORS configuration - allow all origins
corsHandler := handlers.CORS(
handlers.AllowedOrigins([]string{"*"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token"}),
handlers.AllowCredentials(),
handlers.ExposedHeaders([]string{"Authorization", "Content-Type"}),
handlers.AllowedOrigins([]string{
"*", // Allow all origins
}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"}),
handlers.AllowedHeaders([]string{
"Authorization",
"Content-Type",
"Accept",
"Origin",
"X-Requested-With",
"X-CSRF-Token",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Headers",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Credentials",
}),
handlers.ExposedHeaders([]string{
"Authorization",
"Content-Type",
"X-Total-Count",
}),
handlers.MaxAge(3600),
)
corsHandler(router).ServeHTTP(w, r)

48
main.go
View File

@@ -32,11 +32,12 @@ func main() {
defer database.Disconnect()
tmdbService := services.NewTMDBService(cfg.TMDBAccessToken)
kpService := services.NewKinopoiskService(cfg.KPAPIKey, cfg.KPAPIBaseURL)
emailService := services.NewEmailService(cfg)
authService := services.NewAuthService(db, cfg.JWTSecret, emailService, cfg.BaseURL, cfg.GoogleClientID, cfg.GoogleClientSecret, cfg.GoogleRedirectURL, cfg.FrontendURL)
movieService := services.NewMovieService(db, tmdbService)
tvService := services.NewTVService(db, tmdbService)
movieService := services.NewMovieService(db, tmdbService, kpService)
tvService := services.NewTVService(db, tmdbService, kpService)
favoritesService := services.NewFavoritesService(db, tmdbService)
torrentService := services.NewTorrentServiceWithConfig(cfg.RedAPIBaseURL, cfg.RedAPIKey)
reactionsService := services.NewReactionsService(db)
@@ -46,7 +47,7 @@ func main() {
tvHandler := appHandlers.NewTVHandler(tvService)
favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService, cfg)
docsHandler := appHandlers.NewDocsHandler()
searchHandler := appHandlers.NewSearchHandler(tmdbService)
searchHandler := appHandlers.NewSearchHandler(tmdbService, kpService)
categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService)
playersHandler := appHandlers.NewPlayersHandler(cfg)
torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService)
@@ -67,6 +68,7 @@ func main() {
api.HandleFunc("/auth/resend-code", authHandler.ResendVerificationCode).Methods("POST")
api.HandleFunc("/auth/google/login", authHandler.GoogleLogin).Methods("GET")
api.HandleFunc("/auth/google/callback", authHandler.GoogleCallback).Methods("GET")
api.HandleFunc("/auth/refresh", authHandler.RefreshToken).Methods("POST")
api.HandleFunc("/search/multi", searchHandler.MultiSearch).Methods("GET")
@@ -74,9 +76,13 @@ func main() {
api.HandleFunc("/categories/{id}/movies", categoriesHandler.GetMoviesByCategory).Methods("GET")
api.HandleFunc("/categories/{id}/media", categoriesHandler.GetMediaByCategory).Methods("GET")
api.HandleFunc("/players/alloha/{imdb_id}", playersHandler.GetAllohaPlayer).Methods("GET")
api.HandleFunc("/players/lumex/{imdb_id}", playersHandler.GetLumexPlayer).Methods("GET")
api.HandleFunc("/players/vibix/{imdb_id}", playersHandler.GetVibixPlayer).Methods("GET")
api.HandleFunc("/players/alloha/{id_type}/{id}", playersHandler.GetAllohaPlayer).Methods("GET")
api.HandleFunc("/players/lumex/{id_type}/{id}", playersHandler.GetLumexPlayer).Methods("GET")
api.HandleFunc("/players/vibix/{id_type}/{id}", playersHandler.GetVibixPlayer).Methods("GET")
api.HandleFunc("/players/vidsrc/{media_type}/{imdb_id}", playersHandler.GetVidsrcPlayer).Methods("GET")
api.HandleFunc("/players/vidlink/movie/{imdb_id}", playersHandler.GetVidlinkMoviePlayer).Methods("GET")
api.HandleFunc("/players/vidlink/tv/{tmdb_id}", playersHandler.GetVidlinkTVPlayer).Methods("GET")
api.HandleFunc("/players/hdvb/{id_type}/{id}", playersHandler.GetHDVBPlayer).Methods("GET")
api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET")
api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET")
@@ -120,18 +126,38 @@ func main() {
protected.HandleFunc("/auth/profile", authHandler.GetProfile).Methods("GET")
protected.HandleFunc("/auth/profile", authHandler.UpdateProfile).Methods("PUT")
protected.HandleFunc("/auth/profile", authHandler.DeleteAccount).Methods("DELETE")
protected.HandleFunc("/auth/revoke-token", authHandler.RevokeRefreshToken).Methods("POST")
protected.HandleFunc("/auth/revoke-all-tokens", authHandler.RevokeAllRefreshTokens).Methods("POST")
protected.HandleFunc("/reactions/{mediaType}/{mediaId}/my-reaction", reactionsHandler.GetMyReaction).Methods("GET")
protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.SetReaction).Methods("POST")
protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.RemoveReaction).Methods("DELETE")
protected.HandleFunc("/reactions/my", reactionsHandler.GetMyReactions).Methods("GET")
// CORS configuration - allow all origins
corsHandler := handlers.CORS(
handlers.AllowedOrigins([]string{"*"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token"}),
handlers.AllowCredentials(),
handlers.ExposedHeaders([]string{"Authorization", "Content-Type"}),
handlers.AllowedOrigins([]string{
"*", // Allow all origins
}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"}),
handlers.AllowedHeaders([]string{
"Authorization",
"Content-Type",
"Accept",
"Origin",
"X-Requested-With",
"X-CSRF-Token",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Headers",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Credentials",
}),
handlers.ExposedHeaders([]string{
"Authorization",
"Content-Type",
"X-Total-Count",
}),
handlers.MaxAge(3600),
)
var finalHandler http.Handler

View File

@@ -25,6 +25,9 @@ type Config struct {
FrontendURL string
VibixHost string
VibixToken string
KPAPIKey string
HDVBToken string
KPAPIBaseURL string
}
func New() *Config {
@@ -50,6 +53,9 @@ func New() *Config {
FrontendURL: getEnv(EnvFrontendURL, ""),
VibixHost: getEnv(EnvVibixHost, DefaultVibixHost),
VibixToken: getEnv(EnvVibixToken, ""),
KPAPIKey: getEnv(EnvKPAPIKey, ""),
HDVBToken: getEnv(EnvHDVBToken, ""),
KPAPIBaseURL: getEnv("KPAPI_BASE_URL", DefaultKPAPIBase),
}
}

View File

@@ -2,25 +2,27 @@ package config
const (
// Environment variable keys
EnvTMDBAccessToken = "TMDB_ACCESS_TOKEN"
EnvJWTSecret = "JWT_SECRET"
EnvPort = "PORT"
EnvBaseURL = "BASE_URL"
EnvNodeEnv = "NODE_ENV"
EnvGmailUser = "GMAIL_USER"
EnvGmailPassword = "GMAIL_APP_PASSWORD"
EnvLumexURL = "LUMEX_URL"
EnvAllohaToken = "ALLOHA_TOKEN"
EnvRedAPIBaseURL = "REDAPI_BASE_URL"
EnvRedAPIKey = "REDAPI_KEY"
EnvMongoDBName = "MONGO_DB_NAME"
EnvGoogleClientID = "GOOGLE_CLIENT_ID"
EnvGoogleClientSecret= "GOOGLE_CLIENT_SECRET"
EnvGoogleRedirectURL = "GOOGLE_REDIRECT_URL"
EnvFrontendURL = "FRONTEND_URL"
EnvVibixHost = "VIBIX_HOST"
EnvVibixToken = "VIBIX_TOKEN"
EnvTMDBAccessToken = "TMDB_ACCESS_TOKEN"
EnvJWTSecret = "JWT_SECRET"
EnvPort = "PORT"
EnvBaseURL = "BASE_URL"
EnvNodeEnv = "NODE_ENV"
EnvGmailUser = "GMAIL_USER"
EnvGmailPassword = "GMAIL_APP_PASSWORD"
EnvLumexURL = "LUMEX_URL"
EnvAllohaToken = "ALLOHA_TOKEN"
EnvRedAPIBaseURL = "REDAPI_BASE_URL"
EnvRedAPIKey = "REDAPI_KEY"
EnvMongoDBName = "MONGO_DB_NAME"
EnvGoogleClientID = "GOOGLE_CLIENT_ID"
EnvGoogleClientSecret = "GOOGLE_CLIENT_SECRET"
EnvGoogleRedirectURL = "GOOGLE_REDIRECT_URL"
EnvFrontendURL = "FRONTEND_URL"
EnvVibixHost = "VIBIX_HOST"
EnvVibixToken = "VIBIX_TOKEN"
EnvKPAPIKey = "KPAPI_KEY"
EnvHDVBToken = "HDVB_TOKEN"
// Default values
DefaultJWTSecret = "your-secret-key"
DefaultPort = "3000"
@@ -28,9 +30,10 @@ const (
DefaultNodeEnv = "development"
DefaultRedAPIBase = "http://redapi.cfhttp.top"
DefaultMongoDBName = "database"
DefaultVibixHost = "https://vibix.org"
DefaultVibixHost = "https://vibix.org"
DefaultKPAPIBase = "https://kinopoiskapiunofficial.tech/api"
// Static constants
TMDBImageBaseURL = "https://image.tmdb.org/t/p"
CubAPIBaseURL = "https://cub.rip/api"
)
)

View File

@@ -38,4 +38,4 @@ func Disconnect() error {
return client.Disconnect(ctx)
}
func GetClient() *mongo.Client { return client }
func GetClient() *mongo.Client { return client }

View File

@@ -3,8 +3,8 @@ package handlers
import (
"encoding/json"
"net/http"
"time"
"strings"
"time"
"go.mongodb.org/mongo-driver/bson"
@@ -46,7 +46,14 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
return
}
response, err := h.authService.Login(req)
// Получаем информацию о клиенте для refresh токена
userAgent := r.Header.Get("User-Agent")
ipAddress := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
ipAddress = forwarded
}
response, err := h.authService.LoginWithTokens(req, userAgent, ipAddress)
if err != nil {
statusCode := http.StatusBadRequest
if err.Error() == "Account not activated. Please verify your email." {
@@ -74,7 +81,7 @@ func (h *AuthHandler) GoogleLogin(w http.ResponseWriter, r *http.Request) {
func (h *AuthHandler) GoogleCallback(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
state := q.Get("state")
code := q.Get("code")
code := q.Get("code")
preferJSON := q.Get("response") == "json" || strings.Contains(r.Header.Get("Accept"), "application/json")
cookie, _ := r.Cookie("oauth_state")
if cookie == nil || cookie.Value != state || code == "" {
@@ -221,5 +228,82 @@ func (h *AuthHandler) ResendVerificationCode(w http.ResponseWriter, r *http.Requ
json.NewEncoder(w).Encode(response)
}
// RefreshToken refreshes an access token using a refresh token
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
var req models.RefreshTokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Получаем информацию о клиенте
userAgent := r.Header.Get("User-Agent")
ipAddress := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
ipAddress = forwarded
}
tokenPair, err := h.authService.RefreshAccessToken(req.RefreshToken, userAgent, ipAddress)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Data: tokenPair,
Message: "Token refreshed successfully",
})
}
// RevokeRefreshToken revokes a specific refresh token
func (h *AuthHandler) RevokeRefreshToken(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
return
}
var req models.RefreshTokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
err := h.authService.RevokeRefreshToken(userID, req.RefreshToken)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Message: "Refresh token revoked successfully",
})
}
// RevokeAllRefreshTokens revokes all refresh tokens for the current user
func (h *AuthHandler) RevokeAllRefreshTokens(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
return
}
err := h.authService.RevokeAllRefreshTokens(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Message: "All refresh tokens revoked successfully",
})
}
// helpers
func generateState() string { return uuidNew() }
func generateState() string { return uuidNew() }

View File

@@ -4,4 +4,4 @@ import (
"github.com/google/uuid"
)
func uuidNew() string { return uuid.New().String() }
func uuidNew() string { return uuid.New().String() }

View File

@@ -119,4 +119,4 @@ func generateSlug(name string) string {
}
}
return result
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,4 +26,4 @@ func HealthCheck(w http.ResponseWriter, r *http.Request) {
})
}
var startTime = time.Now()
var startTime = time.Now()

View File

@@ -131,4 +131,4 @@ func (h *ImagesHandler) isValidSize(size string, validSizes []string) bool {
}
}
return false
}
}

View File

@@ -0,0 +1,35 @@
package handlers
import (
"net/http"
)
// GetLanguage extracts the lang parameter from request and returns it with default "ru"
// Supports both "lang" and "language" query parameters
// Valid values: "ru", "en"
// Default: "ru"
func GetLanguage(r *http.Request) string {
// Check "lang" parameter first (our new standard)
lang := r.URL.Query().Get("lang")
// Fall back to "language" for backward compatibility
if lang == "" {
lang = r.URL.Query().Get("language")
}
// Default to "ru" if not specified
if lang == "" {
return "ru-RU"
}
// Convert short codes to TMDB format
switch lang {
case "en":
return "en-US"
case "ru":
return "ru-RU"
default:
// Return as-is if already in correct format
return lang
}
}

View File

@@ -29,7 +29,7 @@ func (h *MovieHandler) Search(w http.ResponseWriter, r *http.Request) {
}
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
region := r.URL.Query().Get("region")
year := getIntQuery(r, "year", 0)
@@ -54,9 +54,10 @@ func (h *MovieHandler) GetByID(w http.ResponseWriter, r *http.Request) {
return
}
language := r.URL.Query().Get("language")
language := GetLanguage(r)
idType := r.URL.Query().Get("id_type") // kp or tmdb
movie, err := h.movieService.GetByID(id, language)
movie, err := h.movieService.GetByID(id, language, idType)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -71,7 +72,7 @@ func (h *MovieHandler) GetByID(w http.ResponseWriter, r *http.Request) {
func (h *MovieHandler) Popular(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
region := r.URL.Query().Get("region")
movies, err := h.movieService.GetPopular(page, language, region)
@@ -89,7 +90,7 @@ func (h *MovieHandler) Popular(w http.ResponseWriter, r *http.Request) {
func (h *MovieHandler) TopRated(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
region := r.URL.Query().Get("region")
movies, err := h.movieService.GetTopRated(page, language, region)
@@ -107,7 +108,7 @@ func (h *MovieHandler) TopRated(w http.ResponseWriter, r *http.Request) {
func (h *MovieHandler) Upcoming(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
region := r.URL.Query().Get("region")
movies, err := h.movieService.GetUpcoming(page, language, region)
@@ -125,7 +126,7 @@ func (h *MovieHandler) Upcoming(w http.ResponseWriter, r *http.Request) {
func (h *MovieHandler) NowPlaying(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
region := r.URL.Query().Get("region")
movies, err := h.movieService.GetNowPlaying(page, language, region)
@@ -150,7 +151,7 @@ func (h *MovieHandler) GetRecommendations(w http.ResponseWriter, r *http.Request
}
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
movies, err := h.movieService.GetRecommendations(id, page, language)
if err != nil {
@@ -174,7 +175,7 @@ func (h *MovieHandler) GetSimilar(w http.ResponseWriter, r *http.Request) {
}
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
movies, err := h.movieService.GetSimilar(id, page, language)
if err != nil {
@@ -189,8 +190,6 @@ func (h *MovieHandler) GetSimilar(w http.ResponseWriter, r *http.Request) {
})
}
func (h *MovieHandler) GetExternalIDs(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
@@ -217,11 +216,11 @@ func getIntQuery(r *http.Request, key string, defaultValue int) int {
if str == "" {
return defaultValue
}
value, err := strconv.Atoi(str)
if err != nil {
return defaultValue
}
return value
}
}

View File

@@ -7,11 +7,13 @@ import (
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"neomovies-api/pkg/config"
"github.com/gorilla/mux"
"neomovies-api/pkg/config"
"neomovies-api/pkg/players"
)
type PlayersHandler struct {
@@ -26,29 +28,35 @@ func NewPlayersHandler(cfg *config.Config) *PlayersHandler {
func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetAllohaPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
log.Printf("Route vars: %+v", vars)
imdbID := vars["imdb_id"]
if imdbID == "" {
log.Printf("Error: imdb_id is empty")
http.Error(w, "imdb_id path param is required", http.StatusBadRequest)
idType := vars["id_type"]
id := vars["id"]
if idType == "" || id == "" {
log.Printf("Error: id_type or id is empty")
http.Error(w, "id_type and id are required", http.StatusBadRequest)
return
}
log.Printf("Processing imdb_id: %s", imdbID)
if idType != "kp" && idType != "imdb" {
log.Printf("Error: invalid id_type: %s", idType)
http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest)
return
}
log.Printf("Processing %s ID: %s", idType, id)
if h.config.AllohaToken == "" {
log.Printf("Error: ALLOHA_TOKEN is missing")
http.Error(w, "Server misconfiguration: ALLOHA_TOKEN missing", http.StatusInternalServerError)
return
}
idParam := fmt.Sprintf("imdb=%s", url.QueryEscape(imdbID))
idParam := fmt.Sprintf("%s=%s", idType, url.QueryEscape(id))
apiURL := fmt.Sprintf("https://api.alloha.tv/?token=%s&%s", h.config.AllohaToken, idParam)
log.Printf("Calling Alloha API: %s", apiURL)
resp, err := http.Get(apiURL)
if err != nil {
log.Printf("Error calling Alloha API: %v", err)
@@ -56,119 +64,166 @@ func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request)
return
}
defer resp.Body.Close()
log.Printf("Alloha API response status: %d", resp.StatusCode)
if resp.StatusCode != http.StatusOK {
http.Error(w, fmt.Sprintf("Alloha API error: %d", resp.StatusCode), http.StatusBadGateway)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading Alloha response: %v", err)
http.Error(w, "Failed to read Alloha response", http.StatusInternalServerError)
return
}
log.Printf("Alloha API response body: %s", string(body))
var allohaResponse struct {
Status string `json:"status"`
Data struct {
Data struct {
Iframe string `json:"iframe"`
} `json:"data"`
}
if err := json.Unmarshal(body, &allohaResponse); err != nil {
log.Printf("Error unmarshaling JSON: %v", err)
http.Error(w, "Invalid JSON from Alloha", http.StatusBadGateway)
return
}
if allohaResponse.Status != "success" || allohaResponse.Data.Iframe == "" {
log.Printf("Video not found or empty iframe")
http.Error(w, "Video not found", http.StatusNotFound)
return
}
iframeCode := allohaResponse.Data.Iframe
if !strings.Contains(iframeCode, "<") {
iframeCode = fmt.Sprintf(`<iframe src="%s" allowfullscreen style="border:none;width:100%%;height:100%%"></iframe>`, iframeCode)
// Получаем параметры для сериалов
season := r.URL.Query().Get("season")
episode := r.URL.Query().Get("episode")
translation := r.URL.Query().Get("translation")
if translation == "" {
translation = "66" // дефолтная озвучка
}
// Используем iframe URL из API
iframeCode := allohaResponse.Data.Iframe
// Если это не HTML код, а просто URL
var playerURL string
if !strings.Contains(iframeCode, "<") {
playerURL = iframeCode
// Добавляем параметры для сериалов
if season != "" && episode != "" {
separator := "?"
if strings.Contains(playerURL, "?") {
separator = "&"
}
playerURL = fmt.Sprintf("%s%sseason=%s&episode=%s&translation=%s", playerURL, separator, season, episode, translation)
}
iframeCode = fmt.Sprintf(`<iframe src="%s" allowfullscreen style="border:none;width:100%%;height:100%%"></iframe>`, playerURL)
}
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Alloha Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframeCode)
// Авто-исправление экранированных кавычек
htmlDoc = strings.ReplaceAll(htmlDoc, `\"`, `"`)
htmlDoc = strings.ReplaceAll(htmlDoc, `\'`, `'`)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served Alloha player for imdb_id: %s", imdbID)
log.Printf("Successfully served Alloha player for %s: %s", idType, id)
}
func (h *PlayersHandler) GetLumexPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetLumexPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
log.Printf("Route vars: %+v", vars)
imdbID := vars["imdb_id"]
if imdbID == "" {
log.Printf("Error: imdb_id is empty")
http.Error(w, "imdb_id path param is required", http.StatusBadRequest)
idType := vars["id_type"]
id := vars["id"]
if idType == "" || id == "" {
log.Printf("Error: id_type or id is empty")
http.Error(w, "id_type and id are required", http.StatusBadRequest)
return
}
log.Printf("Processing imdb_id: %s", imdbID)
if idType != "kp" && idType != "imdb" {
log.Printf("Error: invalid id_type: %s", idType)
http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest)
return
}
log.Printf("Processing %s ID: %s", idType, id)
if h.config.LumexURL == "" {
log.Printf("Error: LUMEX_URL is missing")
http.Error(w, "Server misconfiguration: LUMEX_URL missing", http.StatusInternalServerError)
return
}
url := fmt.Sprintf("%s?imdb_id=%s", h.config.LumexURL, url.QueryEscape(imdbID))
log.Printf("Generated Lumex URL: %s", url)
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, url)
var paramName string
if idType == "kp" {
paramName = "kinopoisk_id"
} else {
paramName = "imdb_id"
}
playerURL := fmt.Sprintf("%s?%s=%s", h.config.LumexURL, paramName, id)
log.Printf("Lumex URL: %s", playerURL)
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, playerURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Lumex Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served Lumex player for imdb_id: %s", imdbID)
log.Printf("Successfully served Lumex player for %s: %s", idType, id)
}
func (h *PlayersHandler) GetVibixPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetVibixPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
log.Printf("Route vars: %+v", vars)
imdbID := vars["imdb_id"]
if imdbID == "" {
log.Printf("Error: imdb_id is empty")
http.Error(w, "imdb_id path param is required", http.StatusBadRequest)
idType := vars["id_type"]
id := vars["id"]
if idType == "" || id == "" {
log.Printf("Error: id_type or id is empty")
http.Error(w, "id_type and id are required", http.StatusBadRequest)
return
}
log.Printf("Processing imdb_id: %s", imdbID)
if idType != "kp" && idType != "imdb" {
log.Printf("Error: invalid id_type: %s", idType)
http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest)
return
}
log.Printf("Processing %s ID: %s", idType, id)
if h.config.VibixToken == "" {
log.Printf("Error: VIBIX_TOKEN is missing")
http.Error(w, "Server misconfiguration: VIBIX_TOKEN missing", http.StatusInternalServerError)
return
}
vibixHost := h.config.VibixHost
if vibixHost == "" {
vibixHost = "https://vibix.org"
}
apiURL := fmt.Sprintf("%s/api/v1/publisher/videos/imdb/%s", vibixHost, imdbID)
var endpoint string
if idType == "kp" {
endpoint = "kinopoisk"
} else {
endpoint = "imdb"
}
apiURL := fmt.Sprintf("%s/api/v1/publisher/videos/%s/%s", vibixHost, endpoint, id)
log.Printf("Calling Vibix API: %s", apiURL)
req, err := http.NewRequest("GET", apiURL, nil)
@@ -177,9 +232,9 @@ func (h *PlayersHandler) GetVibixPlayer(w http.ResponseWriter, r *http.Request)
http.Error(w, "Failed to create request", http.StatusInternalServerError)
return
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+h.config.VibixToken)
req.Header.Set("Authorization", h.config.VibixToken)
req.Header.Set("X-CSRF-TOKEN", "")
client := &http.Client{Timeout: 8 * time.Second}
@@ -205,33 +260,458 @@ func (h *PlayersHandler) GetVibixPlayer(w http.ResponseWriter, r *http.Request)
http.Error(w, "Failed to read Vibix response", http.StatusInternalServerError)
return
}
log.Printf("Vibix API response body: %s", string(body))
var vibixResponse struct {
ID interface{} `json:"id"`
IframeURL string `json:"iframe_url"`
}
if err := json.Unmarshal(body, &vibixResponse); err != nil {
log.Printf("Error unmarshaling Vibix JSON: %v", err)
http.Error(w, "Invalid JSON from Vibix", http.StatusBadGateway)
return
}
if vibixResponse.ID == nil || vibixResponse.IframeURL == "" {
log.Printf("Video not found or empty iframe_url")
http.Error(w, "Video not found", http.StatusNotFound)
return
}
log.Printf("Generated Vibix iframe URL: %s", vibixResponse.IframeURL)
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, vibixResponse.IframeURL)
// Vibix использует только iframe_url без season/episode
playerURL := vibixResponse.IframeURL
log.Printf("🔗 Vibix iframe URL: %s", playerURL)
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, playerURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Vibix Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served Vibix player for imdb_id: %s", imdbID)
}
log.Printf("Successfully served Vibix player for %s: %s", idType, id)
}
// GetRgShowsPlayer handles RgShows streaming requests
func (h *PlayersHandler) GetRgShowsPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetRgShowsPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
tmdbID := vars["tmdb_id"]
if tmdbID == "" {
log.Printf("Error: tmdb_id is empty")
http.Error(w, "tmdb_id path param is required", http.StatusBadRequest)
return
}
log.Printf("Processing tmdb_id: %s", tmdbID)
pm := players.NewPlayersManager()
result, err := pm.GetMovieStreamByProvider("rgshows", tmdbID)
if err != nil {
log.Printf("Error getting RgShows stream: %v", err)
http.Error(w, "Failed to get stream", http.StatusInternalServerError)
return
}
if !result.Success {
log.Printf("RgShows stream not found: %s", result.Error)
http.Error(w, "Stream not found", http.StatusNotFound)
return
}
// Create iframe with the stream URL
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, result.StreamURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>RgShows Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served RgShows player for tmdb_id: %s", tmdbID)
}
// GetRgShowsTVPlayer handles RgShows TV show streaming requests
func (h *PlayersHandler) GetRgShowsTVPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetRgShowsTVPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
tmdbID := vars["tmdb_id"]
seasonStr := vars["season"]
episodeStr := vars["episode"]
if tmdbID == "" || seasonStr == "" || episodeStr == "" {
log.Printf("Error: missing required parameters")
http.Error(w, "tmdb_id, season, and episode path params are required", http.StatusBadRequest)
return
}
season, err := strconv.Atoi(seasonStr)
if err != nil {
log.Printf("Error parsing season: %v", err)
http.Error(w, "Invalid season number", http.StatusBadRequest)
return
}
episode, err := strconv.Atoi(episodeStr)
if err != nil {
log.Printf("Error parsing episode: %v", err)
http.Error(w, "Invalid episode number", http.StatusBadRequest)
return
}
log.Printf("Processing tmdb_id: %s, season: %d, episode: %d", tmdbID, season, episode)
pm := players.NewPlayersManager()
result, err := pm.GetTVStreamByProvider("rgshows", tmdbID, season, episode)
if err != nil {
log.Printf("Error getting RgShows TV stream: %v", err)
http.Error(w, "Failed to get stream", http.StatusInternalServerError)
return
}
if !result.Success {
log.Printf("RgShows TV stream not found: %s", result.Error)
http.Error(w, "Stream not found", http.StatusNotFound)
return
}
// Create iframe with the stream URL
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, result.StreamURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>RgShows TV Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served RgShows TV player for tmdb_id: %s, S%dE%d", tmdbID, season, episode)
}
// GetIframeVideoPlayer handles IframeVideo streaming requests
func (h *PlayersHandler) GetIframeVideoPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetIframeVideoPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
kinopoiskID := vars["kinopoisk_id"]
imdbID := vars["imdb_id"]
if kinopoiskID == "" && imdbID == "" {
log.Printf("Error: both kinopoisk_id and imdb_id are empty")
http.Error(w, "Either kinopoisk_id or imdb_id path param is required", http.StatusBadRequest)
return
}
log.Printf("Processing kinopoisk_id: %s, imdb_id: %s", kinopoiskID, imdbID)
pm := players.NewPlayersManager()
result, err := pm.GetStreamWithKinopoisk(kinopoiskID, imdbID)
if err != nil {
log.Printf("Error getting IframeVideo stream: %v", err)
http.Error(w, "Failed to get stream", http.StatusInternalServerError)
return
}
if !result.Success {
log.Printf("IframeVideo stream not found: %s", result.Error)
http.Error(w, "Stream not found", http.StatusNotFound)
return
}
// Create iframe with the stream URL
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, result.StreamURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>IframeVideo Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served IframeVideo player for kinopoisk_id: %s, imdb_id: %s", kinopoiskID, imdbID)
}
// GetStreamAPI returns stream information as JSON API
func (h *PlayersHandler) GetStreamAPI(w http.ResponseWriter, r *http.Request) {
log.Printf("GetStreamAPI called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
provider := vars["provider"]
tmdbID := vars["tmdb_id"]
if provider == "" || tmdbID == "" {
log.Printf("Error: missing required parameters")
http.Error(w, "provider and tmdb_id path params are required", http.StatusBadRequest)
return
}
// Check for TV show parameters
seasonStr := r.URL.Query().Get("season")
episodeStr := r.URL.Query().Get("episode")
kinopoiskID := r.URL.Query().Get("kinopoisk_id")
imdbID := r.URL.Query().Get("imdb_id")
log.Printf("Processing provider: %s, tmdb_id: %s", provider, tmdbID)
pm := players.NewPlayersManager()
var result *players.StreamResult
var err error
switch provider {
case "iframevideo":
if kinopoiskID == "" && imdbID == "" {
http.Error(w, "kinopoisk_id or imdb_id query param is required for IframeVideo", http.StatusBadRequest)
return
}
result, err = pm.GetStreamWithKinopoisk(kinopoiskID, imdbID)
case "rgshows":
if seasonStr != "" && episodeStr != "" {
season, err1 := strconv.Atoi(seasonStr)
episode, err2 := strconv.Atoi(episodeStr)
if err1 != nil || err2 != nil {
http.Error(w, "Invalid season or episode number", http.StatusBadRequest)
return
}
result, err = pm.GetTVStreamByProvider("rgshows", tmdbID, season, episode)
} else {
result, err = pm.GetMovieStreamByProvider("rgshows", tmdbID)
}
default:
http.Error(w, "Unsupported provider", http.StatusBadRequest)
return
}
if err != nil {
log.Printf("Error getting stream from %s: %v", provider, err)
result = &players.StreamResult{
Success: false,
Provider: provider,
Error: err.Error(),
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
log.Printf("Successfully served stream API for provider: %s, tmdb_id: %s", provider, tmdbID)
}
// GetVidsrcPlayer handles Vidsrc.to player (uses IMDb ID for both movies and TV shows)
func (h *PlayersHandler) GetVidsrcPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetVidsrcPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
imdbId := vars["imdb_id"]
mediaType := vars["media_type"] // "movie" or "tv"
if imdbId == "" || mediaType == "" {
http.Error(w, "imdb_id and media_type are required", http.StatusBadRequest)
return
}
var playerURL string
if mediaType == "movie" {
playerURL = fmt.Sprintf("https://vidsrc.to/embed/movie/%s", imdbId)
} else if mediaType == "tv" {
season := r.URL.Query().Get("season")
episode := r.URL.Query().Get("episode")
if season == "" || episode == "" {
http.Error(w, "season and episode are required for TV shows", http.StatusBadRequest)
return
}
playerURL = fmt.Sprintf("https://vidsrc.to/embed/tv/%s/%s/%s", imdbId, season, episode)
} else {
http.Error(w, "Invalid media_type. Use 'movie' or 'tv'", http.StatusBadRequest)
return
}
log.Printf("Generated Vidsrc URL: %s", playerURL)
// Используем общий шаблон с кастомными контролами
htmlDoc := getPlayerWithControlsHTML(playerURL, "Vidsrc Player")
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served Vidsrc player for %s: %s", mediaType, imdbId)
}
// GetVidlinkMoviePlayer handles vidlink.pro player for movies (uses IMDb ID)
func (h *PlayersHandler) GetVidlinkMoviePlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetVidlinkMoviePlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
imdbId := vars["imdb_id"]
if imdbId == "" {
http.Error(w, "imdb_id is required", http.StatusBadRequest)
return
}
playerURL := fmt.Sprintf("https://vidlink.pro/movie/%s", imdbId)
log.Printf("Generated Vidlink Movie URL: %s", playerURL)
// Используем общий шаблон с кастомными контролами
htmlDoc := getPlayerWithControlsHTML(playerURL, "Vidlink Player")
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served Vidlink movie player: %s", imdbId)
}
// GetVidlinkTVPlayer handles vidlink.pro player for TV shows (uses TMDB ID)
func (h *PlayersHandler) GetHDVBPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetHDVBPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
idType := vars["id_type"]
id := vars["id"]
if idType == "" || id == "" {
log.Printf("Error: id_type or id is empty")
http.Error(w, "id_type and id are required", http.StatusBadRequest)
return
}
if idType != "kp" && idType != "imdb" {
log.Printf("Error: invalid id_type: %s", idType)
http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest)
return
}
log.Printf("Processing %s ID: %s", idType, id)
if h.config.HDVBToken == "" {
log.Printf("Error: HDVB_TOKEN is missing")
http.Error(w, "Server misconfiguration: HDVB_TOKEN missing", http.StatusInternalServerError)
return
}
var apiURL string
if idType == "kp" {
apiURL = fmt.Sprintf("https://apivb.com/api/videos.json?id_kp=%s&token=%s", id, h.config.HDVBToken)
} else {
apiURL = fmt.Sprintf("https://apivb.com/api/videos.json?imdb_id=%s&token=%s", id, h.config.HDVBToken)
}
log.Printf("HDVB API URL: %s", apiURL)
resp, err := http.Get(apiURL)
if err != nil {
log.Printf("Error fetching HDVB data: %v", err)
http.Error(w, "Failed to fetch player data", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading HDVB response: %v", err)
http.Error(w, "Failed to read player data", http.StatusInternalServerError)
return
}
var hdvbData []map[string]interface{}
if err := json.Unmarshal(body, &hdvbData); err != nil {
log.Printf("Error parsing HDVB JSON: %v, body: %s", err, string(body))
http.Error(w, "Failed to parse player data", http.StatusInternalServerError)
return
}
if len(hdvbData) == 0 {
log.Printf("No HDVB data found for ID: %s", id)
http.Error(w, "No player data found", http.StatusNotFound)
return
}
iframeURL, ok := hdvbData[0]["iframe_url"].(string)
if !ok || iframeURL == "" {
log.Printf("No iframe_url in HDVB response for ID: %s", id)
http.Error(w, "No player URL found", http.StatusNotFound)
return
}
log.Printf("HDVB iframe URL: %s", iframeURL)
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, iframeURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>HDVB Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served HDVB player for %s: %s", idType, id)
}
func (h *PlayersHandler) GetVidlinkTVPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetVidlinkTVPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
tmdbId := vars["tmdb_id"]
if tmdbId == "" {
http.Error(w, "tmdb_id is required", http.StatusBadRequest)
return
}
season := r.URL.Query().Get("season")
episode := r.URL.Query().Get("episode")
if season == "" || episode == "" {
http.Error(w, "season and episode are required for TV shows", http.StatusBadRequest)
return
}
playerURL := fmt.Sprintf("https://vidlink.pro/tv/%s/%s/%s", tmdbId, season, episode)
log.Printf("Generated Vidlink TV URL: %s", playerURL)
// Используем общий шаблон с кастомными контролами
htmlDoc := getPlayerWithControlsHTML(playerURL, "Vidlink Player")
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
log.Printf("Successfully served Vidlink TV player: %s S%sE%s", tmdbId, season, episode)
}
// getPlayerWithControlsHTML возвращает HTML с плеером и overlay для блокировки кликов
func getPlayerWithControlsHTML(playerURL, title string) string {
return fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<title>%s</title>
<style>
html,body{margin:0;height:100%%;overflow:hidden;background:#000;font-family:Arial,sans-serif;}
#container{position:relative;width:100%%;height:100%%;}
#player-iframe{position:absolute;top:0;left:0;width:100%%;height:100%%;border:none;}
#overlay{position:absolute;top:0;left:0;width:100%%;height:100%%;z-index:10;pointer-events:none;}
#controls{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(transparent,rgba(0,0,0,0.8));padding:20px;opacity:0;transition:opacity 0.3s;pointer-events:auto;z-index:20;}
#container:hover #controls{opacity:1;}
.btn{background:rgba(255,255,255,0.2);border:none;color:#fff;padding:12px 20px;margin:0 5px;border-radius:5px;cursor:pointer;font-size:16px;transition:background 0.2s;}
.btn:hover{background:rgba(255,255,255,0.4);}
.btn:active{background:rgba(255,255,255,0.6);}
</style>
</head>
<body>
<div id="container">
<iframe id="player-iframe" src="%s" allowfullscreen allow="autoplay; encrypted-media; fullscreen; picture-in-picture"></iframe>
<div id="overlay"></div>
<div id="controls">
<button class="btn" id="btn-fullscreen" title="Fullscreen">⛶ Fullscreen</button>
</div>
</div>
<script>
const overlay=document.getElementById('overlay');
// Блокируем клики на iframe (защита от рекламы)
overlay.addEventListener('click',(e)=>{e.preventDefault();e.stopPropagation();});
overlay.addEventListener('mousedown',(e)=>{e.preventDefault();e.stopPropagation();});
// Fullscreen
document.getElementById('btn-fullscreen').addEventListener('click',()=>{
if(!document.fullscreenElement){
document.getElementById('container').requestFullscreen();
}else{
document.exitFullscreen();
}
});
</script>
</body>
</html>`, title, playerURL)
}

View File

@@ -85,7 +85,9 @@ func (h *ReactionsHandler) SetReaction(w http.ResponseWriter, r *http.Request) {
return
}
var request struct{ Type string `json:"type"` }
var request struct {
Type string `json:"type"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
@@ -146,4 +148,4 @@ func (h *ReactionsHandler) GetMyReactions(w http.ResponseWriter, r *http.Request
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: reactions})
}
}

View File

@@ -10,11 +10,13 @@ import (
type SearchHandler struct {
tmdbService *services.TMDBService
kpService *services.KinopoiskService
}
func NewSearchHandler(tmdbService *services.TMDBService) *SearchHandler {
func NewSearchHandler(tmdbService *services.TMDBService, kpService *services.KinopoiskService) *SearchHandler {
return &SearchHandler{
tmdbService: tmdbService,
kpService: kpService,
}
}
@@ -26,9 +28,42 @@ func (h *SearchHandler) MultiSearch(w http.ResponseWriter, r *http.Request) {
}
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
if language == "" {
language = "ru-RU"
language := GetLanguage(r)
if services.ShouldUseKinopoisk(language) && h.kpService != nil {
kpSearch, err := h.kpService.SearchFilms(query, page)
if err == nil {
tmdbResp := services.MapKPSearchToTMDBResponse(kpSearch)
multiResults := make([]models.MultiSearchResult, 0)
for _, movie := range tmdbResp.Results {
multiResults = append(multiResults, models.MultiSearchResult{
ID: movie.ID,
MediaType: "movie",
Title: movie.Title,
OriginalTitle: movie.OriginalTitle,
Overview: movie.Overview,
PosterPath: movie.PosterPath,
BackdropPath: movie.BackdropPath,
ReleaseDate: movie.ReleaseDate,
VoteAverage: movie.VoteAverage,
VoteCount: movie.VoteCount,
Popularity: movie.Popularity,
Adult: movie.Adult,
OriginalLanguage: movie.OriginalLanguage,
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Data: models.MultiSearchResponse{
Page: page,
Results: multiResults,
TotalPages: tmdbResp.TotalPages,
TotalResults: tmdbResp.TotalResults,
},
})
return
}
}
results, err := h.tmdbService.SearchMulti(query, page, language)
@@ -42,4 +77,4 @@ func (h *SearchHandler) MultiSearch(w http.ResponseWriter, r *http.Request) {
Success: true,
Data: results,
})
}
}

View File

@@ -123,12 +123,12 @@ func (h *TorrentsHandler) SearchTorrents(w http.ResponseWriter, r *http.Request)
// Группируем сначала по сезонам, затем по качеству внутри каждого сезона
seasonGroups := h.torrentService.GroupBySeason(results.Results)
finalGroups := make(map[string]map[string][]models.TorrentResult)
for season, torrents := range seasonGroups {
qualityGroups := h.torrentService.GroupByQuality(torrents)
finalGroups[season] = qualityGroups
}
response["grouped"] = true
response["groups"] = finalGroups
} else if options.GroupByQuality {
@@ -364,4 +364,4 @@ func (h *TorrentsHandler) SearchByQuery(w http.ResponseWriter, r *http.Request)
Success: true,
Data: response,
})
}
}

View File

@@ -29,7 +29,7 @@ func (h *TVHandler) Search(w http.ResponseWriter, r *http.Request) {
}
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
year := getIntQuery(r, "first_air_date_year", 0)
tvShows, err := h.tvService.Search(query, page, language, year)
@@ -53,9 +53,10 @@ func (h *TVHandler) GetByID(w http.ResponseWriter, r *http.Request) {
return
}
language := r.URL.Query().Get("language")
language := GetLanguage(r)
idType := r.URL.Query().Get("id_type") // kp or tmdb
tvShow, err := h.tvService.GetByID(id, language)
tvShow, err := h.tvService.GetByID(id, language, idType)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -70,7 +71,7 @@ func (h *TVHandler) GetByID(w http.ResponseWriter, r *http.Request) {
func (h *TVHandler) Popular(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
tvShows, err := h.tvService.GetPopular(page, language)
if err != nil {
@@ -87,7 +88,7 @@ func (h *TVHandler) Popular(w http.ResponseWriter, r *http.Request) {
func (h *TVHandler) TopRated(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
tvShows, err := h.tvService.GetTopRated(page, language)
if err != nil {
@@ -104,7 +105,7 @@ func (h *TVHandler) TopRated(w http.ResponseWriter, r *http.Request) {
func (h *TVHandler) OnTheAir(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
tvShows, err := h.tvService.GetOnTheAir(page, language)
if err != nil {
@@ -121,7 +122,7 @@ func (h *TVHandler) OnTheAir(w http.ResponseWriter, r *http.Request) {
func (h *TVHandler) AiringToday(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
tvShows, err := h.tvService.GetAiringToday(page, language)
if err != nil {
@@ -145,7 +146,7 @@ func (h *TVHandler) GetRecommendations(w http.ResponseWriter, r *http.Request) {
}
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
tvShows, err := h.tvService.GetRecommendations(id, page, language)
if err != nil {
@@ -169,7 +170,7 @@ func (h *TVHandler) GetSimilar(w http.ResponseWriter, r *http.Request) {
}
page := getIntQuery(r, "page", 1)
language := r.URL.Query().Get("language")
language := GetLanguage(r)
tvShows, err := h.tvService.GetSimilar(id, page, language)
if err != nil {
@@ -203,4 +204,4 @@ func (h *TVHandler) GetExternalIDs(w http.ResponseWriter, r *http.Request) {
Success: true,
Data: externalIDs,
})
}
}

View File

@@ -60,4 +60,4 @@ func JWTAuth(secret string) func(http.Handler) http.Handler {
func GetUserIDFromContext(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(UserIDKey).(string)
return userID, ok
}
}

View File

@@ -21,4 +21,4 @@ type FavoriteRequest struct {
MediaType string `json:"mediaType" validate:"required,oneof=movie tv"`
Title string `json:"title" validate:"required"`
PosterPath string `json:"posterPath"`
}
}

View File

@@ -40,6 +40,7 @@ type Movie struct {
Tagline string `json:"tagline,omitempty"`
Homepage string `json:"homepage,omitempty"`
IMDbID string `json:"imdb_id,omitempty"`
KinopoiskID int `json:"kinopoisk_id,omitempty"`
BelongsToCollection *Collection `json:"belongs_to_collection,omitempty"`
ProductionCompanies []ProductionCompany `json:"production_companies,omitempty"`
ProductionCountries []ProductionCountry `json:"production_countries,omitempty"`
@@ -76,6 +77,8 @@ type TVShow struct {
CreatedBy []Creator `json:"created_by,omitempty"`
EpisodeRunTime []int `json:"episode_run_time,omitempty"`
Seasons []Season `json:"seasons,omitempty"`
IMDbID string `json:"imdb_id,omitempty"`
KinopoiskID int `json:"kinopoisk_id,omitempty"`
}
// MultiSearchResult для мультипоиска
@@ -119,6 +122,7 @@ type GenresResponse struct {
type ExternalIDs struct {
ID int `json:"id"`
IMDbID string `json:"imdb_id"`
KinopoiskID int `json:"kinopoisk_id,omitempty"`
TVDBID int `json:"tvdb_id,omitempty"`
WikidataID string `json:"wikidata_id"`
FacebookID string `json:"facebook_id"`

View File

@@ -7,21 +7,22 @@ import (
)
type User struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Email string `json:"email" bson:"email" validate:"required,email"`
Password string `json:"-" bson:"password" validate:"required,min=6"`
Name string `json:"name" bson:"name" validate:"required"`
Avatar string `json:"avatar" bson:"avatar"`
Favorites []string `json:"favorites" bson:"favorites"`
Verified bool `json:"verified" bson:"verified"`
VerificationCode string `json:"-" bson:"verificationCode,omitempty"`
VerificationExpires time.Time `json:"-" bson:"verificationExpires,omitempty"`
IsAdmin bool `json:"isAdmin" bson:"isAdmin"`
AdminVerified bool `json:"adminVerified" bson:"adminVerified"`
CreatedAt time.Time `json:"created_at" bson:"createdAt"`
UpdatedAt time.Time `json:"updated_at" bson:"updatedAt"`
Provider string `json:"provider,omitempty" bson:"provider,omitempty"`
GoogleID string `json:"googleId,omitempty" bson:"googleId,omitempty"`
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Email string `json:"email" bson:"email" validate:"required,email"`
Password string `json:"-" bson:"password" validate:"required,min=6"`
Name string `json:"name" bson:"name" validate:"required"`
Avatar string `json:"avatar" bson:"avatar"`
Favorites []string `json:"favorites" bson:"favorites"`
Verified bool `json:"verified" bson:"verified"`
VerificationCode string `json:"-" bson:"verificationCode,omitempty"`
VerificationExpires time.Time `json:"-" bson:"verificationExpires,omitempty"`
IsAdmin bool `json:"isAdmin" bson:"isAdmin"`
AdminVerified bool `json:"adminVerified" bson:"adminVerified"`
CreatedAt time.Time `json:"created_at" bson:"createdAt"`
UpdatedAt time.Time `json:"updated_at" bson:"updatedAt"`
Provider string `json:"provider,omitempty" bson:"provider,omitempty"`
GoogleID string `json:"googleId,omitempty" bson:"googleId,omitempty"`
RefreshTokens []RefreshToken `json:"-" bson:"refreshTokens,omitempty"`
}
type LoginRequest struct {
@@ -36,8 +37,9 @@ type RegisterRequest struct {
}
type AuthResponse struct {
Token string `json:"token"`
User User `json:"user"`
Token string `json:"token"`
RefreshToken string `json:"refreshToken"`
User User `json:"user"`
}
type VerifyEmailRequest struct {
@@ -47,4 +49,21 @@ type VerifyEmailRequest struct {
type ResendCodeRequest struct {
Email string `json:"email" validate:"required,email"`
}
}
type RefreshToken struct {
Token string `json:"token" bson:"token"`
ExpiresAt time.Time `json:"expiresAt" bson:"expiresAt"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
UserAgent string `json:"userAgent,omitempty" bson:"userAgent,omitempty"`
IPAddress string `json:"ipAddress,omitempty" bson:"ipAddress,omitempty"`
}
type TokenPair struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
}
type RefreshTokenRequest struct {
RefreshToken string `json:"refreshToken" validate:"required"`
}

View File

@@ -12,16 +12,16 @@ func RequestMonitor() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Создаем wrapper для ResponseWriter чтобы получить статус код
ww := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
// Выполняем запрос
next.ServeHTTP(ww, r)
// Вычисляем время выполнения
duration := time.Since(start)
// Форматируем URL (обрезаем если слишком длинный)
url := r.URL.Path
if r.URL.RawQuery != "" {
@@ -30,11 +30,11 @@ func RequestMonitor() func(http.Handler) http.Handler {
if len(url) > 60 {
url = url[:57] + "..."
}
// Определяем цвет статуса
statusColor := getStatusColor(ww.statusCode)
methodColor := getMethodColor(r.Method)
// Выводим информацию о запросе
fmt.Printf("\033[2K\r%s%-6s\033[0m %s%-3d\033[0m │ %-60s │ %6.2fms\n",
methodColor, r.Method,
@@ -88,4 +88,4 @@ func getMethodColor(method string) string {
default:
return "\033[37m" // Белый
}
}
}

208
pkg/players/iframevideo.go Normal file
View File

@@ -0,0 +1,208 @@
package players
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"regexp"
"strconv"
"time"
)
// IframeVideoSearchResponse represents the search response from IframeVideo API
type IframeVideoSearchResponse struct {
Results []struct {
CID int `json:"cid"`
Path string `json:"path"`
Type string `json:"type"`
} `json:"results"`
}
// IframeVideoResponse represents the video response from IframeVideo API
type IframeVideoResponse struct {
Source string `json:"src"`
}
// IframeVideoPlayer implements the IframeVideo streaming service
type IframeVideoPlayer struct {
APIHost string
CDNHost string
Client *http.Client
}
// NewIframeVideoPlayer creates a new IframeVideo player instance
func NewIframeVideoPlayer() *IframeVideoPlayer {
return &IframeVideoPlayer{
APIHost: "https://iframe.video",
CDNHost: "https://videoframe.space",
Client: &http.Client{
Timeout: 8 * time.Second,
},
}
}
// GetStream gets streaming URL by Kinopoisk ID and IMDB ID
func (i *IframeVideoPlayer) GetStream(kinopoiskID, imdbID string) (*StreamResult, error) {
// First, search for content
searchResult, err := i.searchContent(kinopoiskID, imdbID)
if err != nil {
return nil, fmt.Errorf("search failed: %w", err)
}
// Get iframe content to extract token
token, err := i.extractToken(searchResult.Path)
if err != nil {
return nil, fmt.Errorf("token extraction failed: %w", err)
}
// Get video URL
return i.getVideoURL(searchResult.CID, token, searchResult.Type)
}
// searchContent searches for content by Kinopoisk and IMDB IDs
func (i *IframeVideoPlayer) searchContent(kinopoiskID, imdbID string) (*struct {
CID int
Path string
Type string
}, error) {
url := fmt.Sprintf("%s/api/v2/search?imdb=%s&kp=%s", i.APIHost, imdbID, kinopoiskID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36")
resp, err := i.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch search results: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status: %d", resp.StatusCode)
}
var searchResp IframeVideoSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(searchResp.Results) == 0 {
return nil, fmt.Errorf("content not found")
}
result := searchResp.Results[0]
return &struct {
CID int
Path string
Type string
}{
CID: result.CID,
Path: result.Path,
Type: result.Type,
}, nil
}
// extractToken extracts token from iframe HTML content
func (i *IframeVideoPlayer) extractToken(path string) (string, error) {
req, err := http.NewRequest("GET", path, nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
// Set headers similar to C# implementation
req.Header.Set("DNT", "1")
req.Header.Set("Referer", i.CDNHost+"/")
req.Header.Set("Sec-Fetch-Dest", "iframe")
req.Header.Set("Sec-Fetch-Mode", "navigate")
req.Header.Set("Sec-Fetch-Site", "cross-site")
req.Header.Set("Upgrade-Insecure-Requests", "1")
req.Header.Set("sec-ch-ua", `"Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36")
resp, err := i.Client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to fetch iframe content: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("iframe returned status: %d", resp.StatusCode)
}
content, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read iframe content: %w", err)
}
// Extract token using regex as in C# implementation
re := regexp.MustCompile(`\/[^\/]+\/([^\/]+)\/iframe`)
matches := re.FindStringSubmatch(string(content))
if len(matches) < 2 {
return "", fmt.Errorf("token not found in iframe content")
}
return matches[1], nil
}
// getVideoURL gets video URL using extracted token
func (i *IframeVideoPlayer) getVideoURL(cid int, token, mediaType string) (*StreamResult, error) {
// Create multipart form data
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
writer.WriteField("token", token)
writer.WriteField("type", mediaType)
writer.WriteField("season", "")
writer.WriteField("episode", "")
writer.WriteField("mobile", "false")
writer.WriteField("id", strconv.Itoa(cid))
writer.WriteField("qt", "480")
contentType := writer.FormDataContentType()
writer.Close()
req, err := http.NewRequest("POST", i.CDNHost+"/loadvideo", &buf)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("Origin", i.CDNHost)
req.Header.Set("Referer", i.CDNHost+"/")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36")
resp, err := i.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch video URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("video API returned status: %d", resp.StatusCode)
}
var videoResp IframeVideoResponse
if err := json.NewDecoder(resp.Body).Decode(&videoResp); err != nil {
return nil, fmt.Errorf("failed to decode video response: %w", err)
}
if videoResp.Source == "" {
return nil, fmt.Errorf("video URL not found")
}
return &StreamResult{
Success: true,
StreamURL: videoResp.Source,
Provider: "IframeVideo",
Type: "direct",
}, nil
}

81
pkg/players/rgshows.go Normal file
View File

@@ -0,0 +1,81 @@
package players
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
// RgShowsResponse represents the response from RgShows API
type RgShowsResponse struct {
Stream *struct {
URL string `json:"url"`
} `json:"stream"`
}
// RgShowsPlayer implements the RgShows streaming service
type RgShowsPlayer struct {
BaseURL string
Client *http.Client
}
// NewRgShowsPlayer creates a new RgShows player instance
func NewRgShowsPlayer() *RgShowsPlayer {
return &RgShowsPlayer{
BaseURL: "https://rgshows.com",
Client: &http.Client{
Timeout: 40 * time.Second,
},
}
}
// GetMovieStream gets streaming URL for a movie by TMDB ID
func (r *RgShowsPlayer) GetMovieStream(tmdbID string) (*StreamResult, error) {
url := fmt.Sprintf("%s/main/movie/%s", r.BaseURL, tmdbID)
return r.fetchStream(url)
}
// GetTVStream gets streaming URL for a TV show episode by TMDB ID, season and episode
func (r *RgShowsPlayer) GetTVStream(tmdbID string, season, episode int) (*StreamResult, error) {
url := fmt.Sprintf("%s/main/tv/%s/%d/%d", r.BaseURL, tmdbID, season, episode)
return r.fetchStream(url)
}
// fetchStream makes HTTP request to RgShows API and extracts stream URL
func (r *RgShowsPlayer) fetchStream(url string) (*StreamResult, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers similar to the C# implementation
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36")
resp, err := r.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch stream: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status: %d", resp.StatusCode)
}
var rgResp RgShowsResponse
if err := json.NewDecoder(resp.Body).Decode(&rgResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if rgResp.Stream == nil || rgResp.Stream.URL == "" {
return nil, fmt.Errorf("stream not found")
}
return &StreamResult{
Success: true,
StreamURL: rgResp.Stream.URL,
Provider: "RgShows",
Type: "direct",
}, nil
}

99
pkg/players/types.go Normal file
View File

@@ -0,0 +1,99 @@
package players
// StreamResult represents the result of a streaming request
type StreamResult struct {
Success bool `json:"success"`
StreamURL string `json:"stream_url,omitempty"`
Provider string `json:"provider"`
Type string `json:"type"` // "direct", "iframe", "hls", etc.
Error string `json:"error,omitempty"`
}
// Player interface defines methods for streaming providers
type Player interface {
GetMovieStream(tmdbID string) (*StreamResult, error)
GetTVStream(tmdbID string, season, episode int) (*StreamResult, error)
}
// PlayersManager manages all available streaming players
type PlayersManager struct {
rgshows *RgShowsPlayer
iframevideo *IframeVideoPlayer
}
// NewPlayersManager creates a new players manager
func NewPlayersManager() *PlayersManager {
return &PlayersManager{
rgshows: NewRgShowsPlayer(),
iframevideo: NewIframeVideoPlayer(),
}
}
// GetMovieStreams tries to get movie streams from all available providers
func (pm *PlayersManager) GetMovieStreams(tmdbID string) []*StreamResult {
var results []*StreamResult
// Try RgShows
if stream, err := pm.rgshows.GetMovieStream(tmdbID); err == nil {
results = append(results, stream)
} else {
results = append(results, &StreamResult{
Success: false,
Provider: "RgShows",
Error: err.Error(),
})
}
return results
}
// GetTVStreams tries to get TV show streams from all available providers
func (pm *PlayersManager) GetTVStreams(tmdbID string, season, episode int) []*StreamResult {
var results []*StreamResult
// Try RgShows
if stream, err := pm.rgshows.GetTVStream(tmdbID, season, episode); err == nil {
results = append(results, stream)
} else {
results = append(results, &StreamResult{
Success: false,
Provider: "RgShows",
Error: err.Error(),
})
}
return results
}
// GetMovieStreamByProvider gets movie stream from specific provider
func (pm *PlayersManager) GetMovieStreamByProvider(provider, tmdbID string) (*StreamResult, error) {
switch provider {
case "rgshows":
return pm.rgshows.GetMovieStream(tmdbID)
default:
return &StreamResult{
Success: false,
Provider: provider,
Error: "provider not found",
}, nil
}
}
// GetTVStreamByProvider gets TV stream from specific provider
func (pm *PlayersManager) GetTVStreamByProvider(provider, tmdbID string, season, episode int) (*StreamResult, error) {
switch provider {
case "rgshows":
return pm.rgshows.GetTVStream(tmdbID, season, episode)
default:
return &StreamResult{
Success: false,
Provider: provider,
Error: "provider not found",
}, nil
}
}
// GetStreamWithKinopoisk gets stream using Kinopoisk ID and IMDB ID (for IframeVideo)
func (pm *PlayersManager) GetStreamWithKinopoisk(kinopoiskID, imdbID string) (*StreamResult, error) {
return pm.iframevideo.GetStream(kinopoiskID, imdbID)
}

View File

@@ -11,6 +11,7 @@ import (
"sync"
"time"
"encoding/json"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
@@ -19,17 +20,16 @@ import (
"golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"encoding/json"
"neomovies-api/pkg/models"
)
// AuthService contains the database connection, JWT secret, and email service.
type AuthService struct {
db *mongo.Database
jwtSecret string
emailService *EmailService
baseURL string
db *mongo.Database
jwtSecret string
emailService *EmailService
baseURL string
googleClientID string
googleClientSecret string
googleRedirectURL string
@@ -38,18 +38,18 @@ type AuthService struct {
// Reaction represents a reaction entry in the database.
type Reaction struct {
MediaID string `bson:"mediaId"`
Type string `bson:"type"`
UserID primitive.ObjectID `bson:"userId"`
MediaID string `bson:"mediaId"`
Type string `bson:"type"`
UserID primitive.ObjectID `bson:"userId"`
}
// NewAuthService creates and initializes a new AuthService.
func NewAuthService(db *mongo.Database, jwtSecret string, emailService *EmailService, baseURL string, googleClientID string, googleClientSecret string, googleRedirectURL string, frontendURL string) *AuthService {
service := &AuthService{
db: db,
jwtSecret: jwtSecret,
emailService: emailService,
baseURL: baseURL,
db: db,
jwtSecret: jwtSecret,
emailService: emailService,
baseURL: baseURL,
googleClientID: googleClientID,
googleClientSecret: googleClientSecret,
googleRedirectURL: googleRedirectURL,
@@ -81,11 +81,11 @@ func (s *AuthService) GetGoogleLoginURL(state string) (string, error) {
}
type googleUserInfo struct {
Sub string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
Picture string `json:"picture"`
EmailVerified bool `json:"email_verified"`
Sub string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
Picture string `json:"picture"`
EmailVerified bool `json:"email_verified"`
}
// BuildFrontendRedirect builds frontend URL for redirect after OAuth; returns false if not configured
@@ -149,19 +149,19 @@ func (s *AuthService) HandleGoogleCallback(ctx context.Context, code string) (*m
if err == mongo.ErrNoDocuments {
// Create new user
user = models.User{
ID: primitive.NewObjectID(),
Email: gUser.Email,
Password: "",
Name: gUser.Name,
Avatar: gUser.Picture,
Favorites: []string{},
Verified: true,
IsAdmin: false,
ID: primitive.NewObjectID(),
Email: gUser.Email,
Password: "",
Name: gUser.Name,
Avatar: gUser.Picture,
Favorites: []string{},
Verified: true,
IsAdmin: false,
AdminVerified: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Provider: "google",
GoogleID: gUser.Sub,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Provider: "google",
GoogleID: gUser.Sub,
}
if _, err := collection.InsertOne(ctx, user); err != nil {
return nil, err
@@ -171,13 +171,17 @@ func (s *AuthService) HandleGoogleCallback(ctx context.Context, code string) (*m
} else {
// Existing user: ensure fields
update := bson.M{
"verified": true,
"provider": "google",
"googleId": gUser.Sub,
"verified": true,
"provider": "google",
"googleId": gUser.Sub,
"updatedAt": time.Now(),
}
if user.Name == "" && gUser.Name != "" { update["name"] = gUser.Name }
if user.Avatar == "" && gUser.Picture != "" { update["avatar"] = gUser.Picture }
if user.Name == "" && gUser.Name != "" {
update["name"] = gUser.Name
}
if user.Avatar == "" && gUser.Picture != "" {
update["avatar"] = gUser.Picture
}
_, _ = collection.UpdateOne(ctx, bson.M{"_id": user.ID}, bson.M{"$set": update})
}
@@ -186,10 +190,16 @@ func (s *AuthService) HandleGoogleCallback(ctx context.Context, code string) (*m
// If we created user above, we already have user.ID set; else fetch updated
_ = collection.FindOne(ctx, bson.M{"email": gUser.Email}).Decode(&user)
}
token, err := s.generateJWT(user.ID.Hex())
if err != nil { return nil, err }
tokenPair, err := s.generateTokenPair(user.ID.Hex(), "", "")
if err != nil {
return nil, err
}
return &models.AuthResponse{ Token: token, User: user }, nil
return &models.AuthResponse{
Token: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
User: user,
}, nil
}
// generateVerificationCode creates a 6-digit verification code.
@@ -216,18 +226,18 @@ func (s *AuthService) Register(req models.RegisterRequest) (map[string]interface
codeExpires := time.Now().Add(10 * time.Minute)
user := models.User{
ID: primitive.NewObjectID(),
Email: req.Email,
Password: string(hashedPassword),
Name: req.Name,
Favorites: []string{},
Verified: false,
VerificationCode: code,
ID: primitive.NewObjectID(),
Email: req.Email,
Password: string(hashedPassword),
Name: req.Name,
Favorites: []string{},
Verified: false,
VerificationCode: code,
VerificationExpires: codeExpires,
IsAdmin: false,
AdminVerified: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
IsAdmin: false,
AdminVerified: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
_, err = collection.InsertOne(context.Background(), user)
@@ -246,9 +256,9 @@ func (s *AuthService) Register(req models.RegisterRequest) (map[string]interface
}
// Login authenticates a user.
func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, error) {
func (s *AuthService) LoginWithTokens(req models.LoginRequest, userAgent, ipAddress string) (*models.AuthResponse, error) {
collection := s.db.Collection("users")
var user models.User
err := collection.FindOne(context.Background(), bson.M{"email": req.Email}).Decode(&user)
if err != nil {
@@ -264,17 +274,23 @@ func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, erro
return nil, errors.New("Invalid password")
}
token, err := s.generateJWT(user.ID.Hex())
tokenPair, err := s.generateTokenPair(user.ID.Hex(), userAgent, ipAddress)
if err != nil {
return nil, err
}
return &models.AuthResponse{
Token: token,
User: user,
Token: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
User: user,
}, nil
}
// Login authenticates a user (legacy method for backward compatibility).
func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, error) {
return s.LoginWithTokens(req, "", "")
}
// GetUserByID retrieves a user by their ID.
func (s *AuthService) GetUserByID(userID string) (*models.User, error) {
collection := s.db.Collection("users")
@@ -320,7 +336,7 @@ func (s *AuthService) UpdateUser(userID string, updates bson.M) (*models.User, e
func (s *AuthService) generateJWT(userID string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
"exp": time.Now().Add(time.Hour * 1).Unix(), // Сократил время жизни до 1 часа
"iat": time.Now().Unix(),
"jti": uuid.New().String(),
}
@@ -329,6 +345,158 @@ func (s *AuthService) generateJWT(userID string) (string, error) {
return token.SignedString([]byte(s.jwtSecret))
}
// generateRefreshToken generates a new refresh token
func (s *AuthService) generateRefreshToken() string {
return uuid.New().String()
}
// generateTokenPair generates both access and refresh tokens
func (s *AuthService) generateTokenPair(userID, userAgent, ipAddress string) (*models.TokenPair, error) {
accessToken, err := s.generateJWT(userID)
if err != nil {
return nil, err
}
refreshToken := s.generateRefreshToken()
// Сохраняем refresh token в базе данных
collection := s.db.Collection("users")
objectID, err := primitive.ObjectIDFromHex(userID)
if err != nil {
return nil, err
}
refreshTokenDoc := models.RefreshToken{
Token: refreshToken,
ExpiresAt: time.Now().Add(time.Hour * 24 * 30), // 30 дней
CreatedAt: time.Now(),
UserAgent: userAgent,
IPAddress: ipAddress,
}
// Удаляем старые истекшие токены и добавляем новый
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": objectID},
bson.M{
"$pull": bson.M{
"refreshTokens": bson.M{
"expiresAt": bson.M{"$lt": time.Now()},
},
},
},
)
if err != nil {
return nil, err
}
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": objectID},
bson.M{
"$push": bson.M{
"refreshTokens": refreshTokenDoc,
},
"$set": bson.M{
"updatedAt": time.Now(),
},
},
)
if err != nil {
return nil, err
}
return &models.TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
}, nil
}
// RefreshAccessToken refreshes an access token using a refresh token
func (s *AuthService) RefreshAccessToken(refreshToken, userAgent, ipAddress string) (*models.TokenPair, error) {
collection := s.db.Collection("users")
// Найти пользователя с данным refresh токеном
var user models.User
err := collection.FindOne(
context.Background(),
bson.M{
"refreshTokens": bson.M{
"$elemMatch": bson.M{
"token": refreshToken,
"expiresAt": bson.M{"$gt": time.Now()},
},
},
},
).Decode(&user)
if err != nil {
return nil, errors.New("invalid or expired refresh token")
}
// Удалить использованный refresh token
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": user.ID},
bson.M{
"$pull": bson.M{
"refreshTokens": bson.M{
"token": refreshToken,
},
},
},
)
if err != nil {
return nil, err
}
// Создать новую пару токенов
return s.generateTokenPair(user.ID.Hex(), userAgent, ipAddress)
}
// RevokeRefreshToken revokes a specific refresh token
func (s *AuthService) RevokeRefreshToken(userID, refreshToken string) error {
collection := s.db.Collection("users")
objectID, err := primitive.ObjectIDFromHex(userID)
if err != nil {
return err
}
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": objectID},
bson.M{
"$pull": bson.M{
"refreshTokens": bson.M{
"token": refreshToken,
},
},
},
)
return err
}
// RevokeAllRefreshTokens revokes all refresh tokens for a user
func (s *AuthService) RevokeAllRefreshTokens(userID string) error {
collection := s.db.Collection("users")
objectID, err := primitive.ObjectIDFromHex(userID)
if err != nil {
return err
}
_, err = collection.UpdateOne(
context.Background(),
bson.M{"_id": objectID},
bson.M{
"$set": bson.M{
"refreshTokens": []models.RefreshToken{},
"updatedAt": time.Now(),
},
},
)
return err
}
// VerifyEmail verifies a user's email with a code.
func (s *AuthService) VerifyEmail(req models.VerifyEmailRequest) (map[string]interface{}, error) {
collection := s.db.Collection("users")
@@ -439,20 +607,20 @@ func (s *AuthService) DeleteAccount(ctx context.Context, userID string) error {
go func(r Reaction) {
defer wg.Done()
url := fmt.Sprintf("%s/reactions/remove/%s/%s", s.baseURL, r.MediaID, r.Type) // Changed from cubAPIURL to baseURL
req, err := http.NewRequestWithContext(ctx, "POST", url, nil) // or "DELETE"
req, err := http.NewRequestWithContext(ctx, "POST", url, nil) // or "DELETE"
if err != nil {
// Log the error but don't stop the process
fmt.Printf("failed to create request for cub.rip: %v\n", err)
return
}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("failed to send request to cub.rip: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
fmt.Printf("cub.rip API responded with status %d: %s\n", resp.StatusCode, body)

View File

@@ -98,7 +98,7 @@ func (s *EmailService) SendVerificationEmail(userEmail, code string) error {
func (s *EmailService) SendPasswordResetEmail(userEmail, resetToken string) error {
resetURL := fmt.Sprintf("%s/reset-password?token=%s", s.config.BaseURL, resetToken)
options := &EmailOptions{
To: []string{userEmail},
Subject: "Сброс пароля Neo Movies",
@@ -147,4 +147,4 @@ func (s *EmailService) SendMovieRecommendationEmail(userEmail, userName string,
}
return s.SendEmail(options)
}
}

261
pkg/services/kinopoisk.go Normal file
View File

@@ -0,0 +1,261 @@
package services
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
)
type KinopoiskService struct {
apiKey string
baseURL string
client *http.Client
}
type KPFilm struct {
KinopoiskId int `json:"kinopoiskId"`
ImdbId string `json:"imdbId"`
NameRu string `json:"nameRu"`
NameEn string `json:"nameEn"`
NameOriginal string `json:"nameOriginal"`
PosterUrl string `json:"posterUrl"`
PosterUrlPreview string `json:"posterUrlPreview"`
CoverUrl string `json:"coverUrl"`
LogoUrl string `json:"logoUrl"`
ReviewsCount int `json:"reviewsCount"`
RatingGoodReview float64 `json:"ratingGoodReview"`
RatingGoodReviewVoteCount int `json:"ratingGoodReviewVoteCount"`
RatingKinopoisk float64 `json:"ratingKinopoisk"`
RatingKinopoiskVoteCount int `json:"ratingKinopoiskVoteCount"`
RatingImdb float64 `json:"ratingImdb"`
RatingImdbVoteCount int `json:"ratingImdbVoteCount"`
RatingFilmCritics float64 `json:"ratingFilmCritics"`
RatingFilmCriticsVoteCount int `json:"ratingFilmCriticsVoteCount"`
RatingAwait float64 `json:"ratingAwait"`
RatingAwaitCount int `json:"ratingAwaitCount"`
RatingRfCritics float64 `json:"ratingRfCritics"`
RatingRfCriticsVoteCount int `json:"ratingRfCriticsVoteCount"`
WebUrl string `json:"webUrl"`
Year int `json:"year"`
FilmLength int `json:"filmLength"`
Slogan string `json:"slogan"`
Description string `json:"description"`
ShortDescription string `json:"shortDescription"`
EditorAnnotation string `json:"editorAnnotation"`
IsTicketsAvailable bool `json:"isTicketsAvailable"`
ProductionStatus string `json:"productionStatus"`
Type string `json:"type"`
RatingMpaa string `json:"ratingMpaa"`
RatingAgeLimits string `json:"ratingAgeLimits"`
HasImax bool `json:"hasImax"`
Has3D bool `json:"has3d"`
LastSync string `json:"lastSync"`
Countries []struct {
Country string `json:"country"`
} `json:"countries"`
Genres []struct {
Genre string `json:"genre"`
} `json:"genres"`
StartYear int `json:"startYear"`
EndYear int `json:"endYear"`
Serial bool `json:"serial"`
ShortFilm bool `json:"shortFilm"`
Completed bool `json:"completed"`
}
type KPSearchResponse struct {
Keyword string `json:"keyword"`
PagesCount int `json:"pagesCount"`
Films []KPFilmShort `json:"films"`
SearchFilmsCountResult int `json:"searchFilmsCountResult"`
}
type KPFilmShort struct {
FilmId int `json:"filmId"`
NameRu string `json:"nameRu"`
NameEn string `json:"nameEn"`
Type string `json:"type"`
Year string `json:"year"`
Description string `json:"description"`
FilmLength string `json:"filmLength"`
Countries []KPCountry `json:"countries"`
Genres []KPGenre `json:"genres"`
Rating string `json:"rating"`
RatingVoteCount int `json:"ratingVoteCount"`
PosterUrl string `json:"posterUrl"`
PosterUrlPreview string `json:"posterUrlPreview"`
}
type KPCountry struct {
Country string `json:"country"`
}
type KPGenre struct {
Genre string `json:"genre"`
}
type KPExternalSource struct {
Source string `json:"source"`
ID string `json:"id"`
}
func NewKinopoiskService(apiKey, baseURL string) *KinopoiskService {
return &KinopoiskService{
apiKey: apiKey,
baseURL: baseURL,
client: &http.Client{Timeout: 10 * time.Second},
}
}
func (s *KinopoiskService) makeRequest(endpoint string, target interface{}) error {
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return err
}
req.Header.Set("X-API-KEY", s.apiKey)
req.Header.Set("Accept", "application/json")
resp, err := s.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Kinopoisk API error: %d", resp.StatusCode)
}
return json.NewDecoder(resp.Body).Decode(target)
}
func (s *KinopoiskService) GetFilmByKinopoiskId(id int) (*KPFilm, error) {
endpoint := fmt.Sprintf("%s/v2.2/films/%d", s.baseURL, id)
var film KPFilm
err := s.makeRequest(endpoint, &film)
return &film, err
}
func (s *KinopoiskService) GetFilmByImdbId(imdbId string) (*KPFilm, error) {
endpoint := fmt.Sprintf("%s/v2.2/films?imdbId=%s", s.baseURL, imdbId)
var response struct {
Films []KPFilm `json:"items"`
}
err := s.makeRequest(endpoint, &response)
if err != nil {
return nil, err
}
if len(response.Films) == 0 {
return nil, fmt.Errorf("film not found")
}
return &response.Films[0], nil
}
func (s *KinopoiskService) SearchFilms(keyword string, page int) (*KPSearchResponse, error) {
endpoint := fmt.Sprintf("%s/v2.1/films/search-by-keyword?keyword=%s&page=%d", s.baseURL, keyword, page)
var response KPSearchResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *KinopoiskService) GetExternalSources(kinopoiskId int) ([]KPExternalSource, error) {
endpoint := fmt.Sprintf("%s/v2.2/films/%d/external_sources", s.baseURL, kinopoiskId)
var response struct {
Items []KPExternalSource `json:"items"`
}
err := s.makeRequest(endpoint, &response)
if err != nil {
return nil, err
}
return response.Items, nil
}
func (s *KinopoiskService) GetTopFilms(topType string, page int) (*KPSearchResponse, error) {
endpoint := fmt.Sprintf("%s/v2.2/films/top?type=%s&page=%d", s.baseURL, topType, page)
var response struct {
PagesCount int `json:"pagesCount"`
Films []KPFilmShort `json:"films"`
}
err := s.makeRequest(endpoint, &response)
if err != nil {
return nil, err
}
return &KPSearchResponse{
PagesCount: response.PagesCount,
Films: response.Films,
SearchFilmsCountResult: len(response.Films),
}, nil
}
func KPIdToImdbId(kpService *KinopoiskService, kpId int) (string, error) {
film, err := kpService.GetFilmByKinopoiskId(kpId)
if err != nil {
return "", err
}
return film.ImdbId, nil
}
func ImdbIdToKPId(kpService *KinopoiskService, imdbId string) (int, error) {
film, err := kpService.GetFilmByImdbId(imdbId)
if err != nil {
return 0, err
}
return film.KinopoiskId, nil
}
func TmdbIdToKPId(tmdbService *TMDBService, kpService *KinopoiskService, tmdbId int) (int, error) {
externalIds, err := tmdbService.GetMovieExternalIDs(tmdbId)
if err != nil {
return 0, err
}
if externalIds.IMDbID == "" {
return 0, fmt.Errorf("no IMDb ID found for TMDB ID %d", tmdbId)
}
return ImdbIdToKPId(kpService, externalIds.IMDbID)
}
func KPIdToTmdbId(tmdbService *TMDBService, kpService *KinopoiskService, kpId int) (int, error) {
imdbId, err := KPIdToImdbId(kpService, kpId)
if err != nil {
return 0, err
}
movies, err := tmdbService.SearchMovies("", 1, "en-US", "", 0)
if err != nil {
return 0, err
}
for _, movie := range movies.Results {
ids, err := tmdbService.GetMovieExternalIDs(movie.ID)
if err != nil {
continue
}
if ids.IMDbID == imdbId {
return movie.ID, nil
}
}
return 0, fmt.Errorf("TMDB ID not found for KP ID %d", kpId)
}
func ConvertKPRating(rating float64) float64 {
return rating
}
func FormatKPYear(year int) string {
return strconv.Itoa(year)
}

307
pkg/services/kp_mapper.go Normal file
View File

@@ -0,0 +1,307 @@
package services
import (
"fmt"
"strconv"
"strings"
"time"
"neomovies-api/pkg/models"
)
func MapKPFilmToTMDBMovie(kpFilm *KPFilm) *models.Movie {
if kpFilm == nil {
return nil
}
releaseDate := ""
if kpFilm.Year > 0 {
releaseDate = fmt.Sprintf("%d-01-01", kpFilm.Year)
}
genres := make([]models.Genre, 0)
for _, g := range kpFilm.Genres {
genres = append(genres, models.Genre{
ID: 0,
Name: g.Genre,
})
}
countries := make([]models.ProductionCountry, 0)
for _, c := range kpFilm.Countries {
countries = append(countries, models.ProductionCountry{
ISO31661: "",
Name: c.Country,
})
}
posterPath := ""
if kpFilm.PosterUrlPreview != "" {
posterPath = kpFilm.PosterUrlPreview
} else if kpFilm.PosterUrl != "" {
posterPath = kpFilm.PosterUrl
}
backdropPath := ""
if kpFilm.CoverUrl != "" {
backdropPath = kpFilm.CoverUrl
}
overview := kpFilm.Description
if overview == "" {
overview = kpFilm.ShortDescription
}
title := kpFilm.NameRu
if title == "" {
title = kpFilm.NameEn
}
if title == "" {
title = kpFilm.NameOriginal
}
originalTitle := kpFilm.NameOriginal
if originalTitle == "" {
originalTitle = kpFilm.NameEn
}
return &models.Movie{
ID: kpFilm.KinopoiskId,
Title: title,
OriginalTitle: originalTitle,
Overview: overview,
PosterPath: posterPath,
BackdropPath: backdropPath,
ReleaseDate: releaseDate,
VoteAverage: kpFilm.RatingKinopoisk,
VoteCount: kpFilm.RatingKinopoiskVoteCount,
Popularity: float64(kpFilm.RatingKinopoisk * 100),
Adult: false,
OriginalLanguage: detectLanguage(kpFilm),
Runtime: kpFilm.FilmLength,
Genres: genres,
Tagline: kpFilm.Slogan,
ProductionCountries: countries,
IMDbID: kpFilm.ImdbId,
KinopoiskID: kpFilm.KinopoiskId,
}
}
func MapKPFilmToTVShow(kpFilm *KPFilm) *models.TVShow {
if kpFilm == nil {
return nil
}
firstAirDate := ""
if kpFilm.StartYear > 0 {
firstAirDate = fmt.Sprintf("%d-01-01", kpFilm.StartYear)
}
lastAirDate := ""
if kpFilm.EndYear > 0 {
lastAirDate = fmt.Sprintf("%d-01-01", kpFilm.EndYear)
}
genres := make([]models.Genre, 0)
for _, g := range kpFilm.Genres {
genres = append(genres, models.Genre{
ID: 0,
Name: g.Genre,
})
}
posterPath := ""
if kpFilm.PosterUrlPreview != "" {
posterPath = kpFilm.PosterUrlPreview
} else if kpFilm.PosterUrl != "" {
posterPath = kpFilm.PosterUrl
}
backdropPath := ""
if kpFilm.CoverUrl != "" {
backdropPath = kpFilm.CoverUrl
}
overview := kpFilm.Description
if overview == "" {
overview = kpFilm.ShortDescription
}
name := kpFilm.NameRu
if name == "" {
name = kpFilm.NameEn
}
if name == "" {
name = kpFilm.NameOriginal
}
originalName := kpFilm.NameOriginal
if originalName == "" {
originalName = kpFilm.NameEn
}
status := "Ended"
if kpFilm.Completed {
status = "Ended"
} else {
status = "Returning Series"
}
return &models.TVShow{
ID: kpFilm.KinopoiskId,
Name: name,
OriginalName: originalName,
Overview: overview,
PosterPath: posterPath,
BackdropPath: backdropPath,
FirstAirDate: firstAirDate,
LastAirDate: lastAirDate,
VoteAverage: kpFilm.RatingKinopoisk,
VoteCount: kpFilm.RatingKinopoiskVoteCount,
Popularity: float64(kpFilm.RatingKinopoisk * 100),
OriginalLanguage: detectLanguage(kpFilm),
Genres: genres,
Status: status,
InProduction: !kpFilm.Completed,
KinopoiskID: kpFilm.KinopoiskId,
}
}
func MapKPSearchToTMDBResponse(kpSearch *KPSearchResponse) *models.TMDBResponse {
if kpSearch == nil {
return &models.TMDBResponse{
Page: 1,
Results: []models.Movie{},
TotalPages: 0,
TotalResults: 0,
}
}
results := make([]models.Movie, 0)
for _, film := range kpSearch.Films {
movie := mapKPFilmShortToMovie(film)
if movie != nil {
results = append(results, *movie)
}
}
totalPages := kpSearch.PagesCount
if totalPages == 0 && len(results) > 0 {
totalPages = 1
}
return &models.TMDBResponse{
Page: 1,
Results: results,
TotalPages: totalPages,
TotalResults: kpSearch.SearchFilmsCountResult,
}
}
func mapKPFilmShortToMovie(film KPFilmShort) *models.Movie {
genres := make([]models.Genre, 0)
for _, g := range film.Genres {
genres = append(genres, models.Genre{
ID: 0,
Name: g.Genre,
})
}
year := 0
if film.Year != "" {
year, _ = strconv.Atoi(film.Year)
}
releaseDate := ""
if year > 0 {
releaseDate = fmt.Sprintf("%d-01-01", year)
}
posterPath := film.PosterUrlPreview
if posterPath == "" {
posterPath = film.PosterUrl
}
title := film.NameRu
if title == "" {
title = film.NameEn
}
originalTitle := film.NameEn
if originalTitle == "" {
originalTitle = film.NameRu
}
rating := 0.0
if film.Rating != "" {
rating, _ = strconv.ParseFloat(film.Rating, 64)
}
return &models.Movie{
ID: film.FilmId,
Title: title,
OriginalTitle: originalTitle,
Overview: film.Description,
PosterPath: posterPath,
ReleaseDate: releaseDate,
VoteAverage: rating,
VoteCount: film.RatingVoteCount,
Popularity: rating * 100,
Genres: genres,
KinopoiskID: film.FilmId,
}
}
func detectLanguage(film *KPFilm) string {
if film.NameRu != "" {
return "ru"
}
if film.NameEn != "" {
return "en"
}
return "ru"
}
func MapKPExternalIDsToTMDB(kpFilm *KPFilm) *models.ExternalIDs {
if kpFilm == nil {
return &models.ExternalIDs{}
}
return &models.ExternalIDs{
ID: kpFilm.KinopoiskId,
IMDbID: kpFilm.ImdbId,
KinopoiskID: kpFilm.KinopoiskId,
}
}
func ShouldUseKinopoisk(language string) bool {
if language == "" {
return false
}
lang := strings.ToLower(language)
return strings.HasPrefix(lang, "ru")
}
func NormalizeLanguage(language string) string {
if language == "" {
return "en-US"
}
lang := strings.ToLower(language)
if strings.HasPrefix(lang, "ru") {
return "ru-RU"
}
return "en-US"
}
func ConvertKPRatingToTMDB(kpRating float64) float64 {
return kpRating
}
func FormatKPDate(year int) string {
if year <= 0 {
return time.Now().Format("2006-01-02")
}
return fmt.Sprintf("%d-01-01", year)
}

View File

@@ -1,34 +1,89 @@
package services
import (
"fmt"
"go.mongodb.org/mongo-driver/mongo"
"neomovies-api/pkg/models"
)
type MovieService struct {
tmdb *TMDBService
tmdb *TMDBService
kpService *KinopoiskService
}
func NewMovieService(db *mongo.Database, tmdb *TMDBService) *MovieService {
func NewMovieService(db *mongo.Database, tmdb *TMDBService, kpService *KinopoiskService) *MovieService {
return &MovieService{
tmdb: tmdb,
tmdb: tmdb,
kpService: kpService,
}
}
func (s *MovieService) Search(query string, page int, language, region string, year int) (*models.TMDBResponse, error) {
if ShouldUseKinopoisk(language) && s.kpService != nil {
kpSearch, err := s.kpService.SearchFilms(query, page)
if err == nil {
return MapKPSearchToTMDBResponse(kpSearch), nil
}
}
return s.tmdb.SearchMovies(query, page, language, region, year)
}
func (s *MovieService) GetByID(id int, language string) (*models.Movie, error) {
func (s *MovieService) GetByID(id int, language string, idType string) (*models.Movie, error) {
// Если указан id_type, используем его; иначе определяем по языку
useKP := false
if idType == "kp" {
useKP = true
} else if idType == "tmdb" {
useKP = false
} else {
// Если id_type не указан, используем старую логику по языку
useKP = ShouldUseKinopoisk(language)
}
if useKP && s.kpService != nil {
// Сначала пробуем напрямую по KP ID
kpFilm, err := s.kpService.GetFilmByKinopoiskId(id)
if err == nil {
return MapKPFilmToTMDBMovie(kpFilm), nil
}
// Если не найдено и явно указан id_type=kp, возможно это TMDB ID
// Пробуем конвертировать TMDB -> KP
if idType == "kp" {
kpId, convErr := TmdbIdToKPId(s.tmdb, s.kpService, id)
if convErr == nil {
kpFilm, err := s.kpService.GetFilmByKinopoiskId(kpId)
if err == nil {
return MapKPFilmToTMDBMovie(kpFilm), nil
}
}
// Если конвертация не удалась, возвращаем ошибку вместо fallback
return nil, fmt.Errorf("film not found in Kinopoisk with id %d", id)
}
}
// Для TMDB или если KP не указан
return s.tmdb.GetMovie(id, language)
}
func (s *MovieService) GetPopular(page int, language, region string) (*models.TMDBResponse, error) {
if ShouldUseKinopoisk(language) && s.kpService != nil {
kpTop, err := s.kpService.GetTopFilms("TOP_100_POPULAR_FILMS", page)
if err == nil {
return MapKPSearchToTMDBResponse(kpTop), nil
}
}
return s.tmdb.GetPopularMovies(page, language, region)
}
func (s *MovieService) GetTopRated(page int, language, region string) (*models.TMDBResponse, error) {
if ShouldUseKinopoisk(language) && s.kpService != nil {
kpTop, err := s.kpService.GetTopFilms("TOP_250_BEST_FILMS", page)
if err == nil {
return MapKPSearchToTMDBResponse(kpTop), nil
}
}
return s.tmdb.GetTopRatedMovies(page, language, region)
}
@@ -48,8 +103,27 @@ func (s *MovieService) GetSimilar(id, page int, language string) (*models.TMDBRe
return s.tmdb.GetSimilarMovies(id, page, language)
}
func (s *MovieService) GetExternalIDs(id int) (*models.ExternalIDs, error) {
return s.tmdb.GetMovieExternalIDs(id)
}
if s.kpService != nil {
kpFilm, err := s.kpService.GetFilmByKinopoiskId(id)
if err == nil && kpFilm != nil {
externalIDs := MapKPExternalIDsToTMDB(kpFilm)
externalIDs.ID = id
return externalIDs, nil
}
}
tmdbIDs, err := s.tmdb.GetMovieExternalIDs(id)
if err != nil {
return nil, err
}
if s.kpService != nil && tmdbIDs.IMDbID != "" {
kpFilm, err := s.kpService.GetFilmByImdbId(tmdbIDs.IMDbID)
if err == nil && kpFilm != nil {
tmdbIDs.KinopoiskID = kpFilm.KinopoiskId
}
}
return tmdbIDs, nil
}

View File

@@ -33,7 +33,7 @@ var validReactions = []string{"fire", "nice", "think", "bore", "shit"}
// Получить счетчики реакций для медиа из внешнего API (cub.rip)
func (s *ReactionsService) GetReactionCounts(mediaType, mediaID string) (*models.ReactionCounts, error) {
cubID := fmt.Sprintf("%s_%s", mediaType, mediaID)
resp, err := s.client.Get(fmt.Sprintf("%s/reactions/get/%s", config.CubAPIBaseURL, cubID))
if err != nil {
return &models.ReactionCounts{}, nil
@@ -83,7 +83,9 @@ func (s *ReactionsService) GetMyReaction(userID, mediaType, mediaID string) (str
collection := s.db.Collection("reactions")
ctx := context.Background()
var result struct{ Type string `bson:"type"` }
var result struct {
Type string `bson:"type"`
}
err := collection.FindOne(ctx, bson.M{
"userId": userID,
"mediaType": mediaType,
@@ -165,7 +167,7 @@ func (s *ReactionsService) isValidReactionType(reactionType string) bool {
// Отправка реакции в cub.rip API (асинхронно)
func (s *ReactionsService) sendReactionToCub(mediaID, reactionType string) {
url := fmt.Sprintf("%s/reactions/set", config.CubAPIBaseURL)
data := map[string]string{
"mediaId": mediaID,
"type": reactionType,
@@ -185,4 +187,4 @@ func (s *ReactionsService) sendReactionToCub(mediaID, reactionType string) {
if resp.StatusCode == http.StatusOK {
fmt.Printf("Reaction sent to cub.rip: %s - %s\n", mediaID, reactionType)
}
}
}

View File

@@ -52,23 +52,23 @@ func (s *TMDBService) SearchMovies(query string, page int, language, region stri
params.Set("query", query)
params.Set("page", strconv.Itoa(page))
params.Set("include_adult", "false")
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
if year > 0 {
params.Set("year", strconv.Itoa(year))
}
endpoint := fmt.Sprintf("%s/search/movie?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -129,19 +129,19 @@ func (s *TMDBService) SearchTVShows(query string, page int, language string, fir
params.Set("query", query)
params.Set("page", strconv.Itoa(page))
params.Set("include_adult", "false")
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if firstAirDateYear > 0 {
params.Set("first_air_date_year", strconv.Itoa(firstAirDateYear))
}
endpoint := fmt.Sprintf("%s/search/tv?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -149,7 +149,7 @@ func (s *TMDBService) SearchTVShows(query string, page int, language string, fir
func (s *TMDBService) GetMovie(id int, language string) (*models.Movie, error) {
params := url.Values{}
if language != "" {
params.Set("language", language)
} else {
@@ -157,7 +157,7 @@ func (s *TMDBService) GetMovie(id int, language string) (*models.Movie, error) {
}
endpoint := fmt.Sprintf("%s/movie/%d?%s", s.baseURL, id, params.Encode())
var movie models.Movie
err := s.makeRequest(endpoint, &movie)
return &movie, err
@@ -165,7 +165,7 @@ func (s *TMDBService) GetMovie(id int, language string) (*models.Movie, error) {
func (s *TMDBService) GetTVShow(id int, language string) (*models.TVShow, error) {
params := url.Values{}
if language != "" {
params.Set("language", language)
} else {
@@ -173,7 +173,7 @@ func (s *TMDBService) GetTVShow(id int, language string) (*models.TVShow, error)
}
endpoint := fmt.Sprintf("%s/tv/%d?%s", s.baseURL, id, params.Encode())
var tvShow models.TVShow
err := s.makeRequest(endpoint, &tvShow)
return &tvShow, err
@@ -181,7 +181,7 @@ func (s *TMDBService) GetTVShow(id int, language string) (*models.TVShow, error)
func (s *TMDBService) GetGenres(mediaType string, language string) (*models.GenresResponse, error) {
params := url.Values{}
if language != "" {
params.Set("language", language)
} else {
@@ -189,7 +189,7 @@ func (s *TMDBService) GetGenres(mediaType string, language string) (*models.Genr
}
endpoint := fmt.Sprintf("%s/genre/%s/list?%s", s.baseURL, mediaType, params.Encode())
var response models.GenresResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -210,11 +210,11 @@ func (s *TMDBService) GetAllGenres() (*models.GenresResponse, error) {
// Объединяем жанры, убирая дубликаты
allGenres := make(map[int]models.Genre)
for _, genre := range movieGenres.Genres {
allGenres[genre.ID] = genre
}
for _, genre := range tvGenres.Genres {
allGenres[genre.ID] = genre
}
@@ -231,19 +231,19 @@ func (s *TMDBService) GetAllGenres() (*models.GenresResponse, error) {
func (s *TMDBService) GetPopularMovies(page int, language, region string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
endpoint := fmt.Sprintf("%s/movie/popular?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -252,19 +252,19 @@ func (s *TMDBService) GetPopularMovies(page int, language, region string) (*mode
func (s *TMDBService) GetTopRatedMovies(page int, language, region string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
endpoint := fmt.Sprintf("%s/movie/top_rated?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -273,19 +273,19 @@ func (s *TMDBService) GetTopRatedMovies(page int, language, region string) (*mod
func (s *TMDBService) GetUpcomingMovies(page int, language, region string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
endpoint := fmt.Sprintf("%s/movie/upcoming?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -294,19 +294,19 @@ func (s *TMDBService) GetUpcomingMovies(page int, language, region string) (*mod
func (s *TMDBService) GetNowPlayingMovies(page int, language, region string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
endpoint := fmt.Sprintf("%s/movie/now_playing?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -315,7 +315,7 @@ func (s *TMDBService) GetNowPlayingMovies(page int, language, region string) (*m
func (s *TMDBService) GetMovieRecommendations(id, page int, language string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
@@ -323,7 +323,7 @@ func (s *TMDBService) GetMovieRecommendations(id, page int, language string) (*m
}
endpoint := fmt.Sprintf("%s/movie/%d/recommendations?%s", s.baseURL, id, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -332,7 +332,7 @@ func (s *TMDBService) GetMovieRecommendations(id, page int, language string) (*m
func (s *TMDBService) GetSimilarMovies(id, page int, language string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
@@ -340,7 +340,7 @@ func (s *TMDBService) GetSimilarMovies(id, page int, language string) (*models.T
}
endpoint := fmt.Sprintf("%s/movie/%d/similar?%s", s.baseURL, id, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -349,7 +349,7 @@ func (s *TMDBService) GetSimilarMovies(id, page int, language string) (*models.T
func (s *TMDBService) GetPopularTVShows(page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
@@ -357,7 +357,7 @@ func (s *TMDBService) GetPopularTVShows(page int, language string) (*models.TMDB
}
endpoint := fmt.Sprintf("%s/tv/popular?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -366,7 +366,7 @@ func (s *TMDBService) GetPopularTVShows(page int, language string) (*models.TMDB
func (s *TMDBService) GetTopRatedTVShows(page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
@@ -374,7 +374,7 @@ func (s *TMDBService) GetTopRatedTVShows(page int, language string) (*models.TMD
}
endpoint := fmt.Sprintf("%s/tv/top_rated?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -383,7 +383,7 @@ func (s *TMDBService) GetTopRatedTVShows(page int, language string) (*models.TMD
func (s *TMDBService) GetOnTheAirTVShows(page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
@@ -391,7 +391,7 @@ func (s *TMDBService) GetOnTheAirTVShows(page int, language string) (*models.TMD
}
endpoint := fmt.Sprintf("%s/tv/on_the_air?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -400,7 +400,7 @@ func (s *TMDBService) GetOnTheAirTVShows(page int, language string) (*models.TMD
func (s *TMDBService) GetAiringTodayTVShows(page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
@@ -408,7 +408,7 @@ func (s *TMDBService) GetAiringTodayTVShows(page int, language string) (*models.
}
endpoint := fmt.Sprintf("%s/tv/airing_today?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -417,7 +417,7 @@ func (s *TMDBService) GetAiringTodayTVShows(page int, language string) (*models.
func (s *TMDBService) GetTVRecommendations(id, page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
@@ -425,7 +425,7 @@ func (s *TMDBService) GetTVRecommendations(id, page int, language string) (*mode
}
endpoint := fmt.Sprintf("%s/tv/%d/recommendations?%s", s.baseURL, id, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -434,7 +434,7 @@ func (s *TMDBService) GetTVRecommendations(id, page int, language string) (*mode
func (s *TMDBService) GetSimilarTVShows(id, page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
@@ -442,7 +442,7 @@ func (s *TMDBService) GetSimilarTVShows(id, page int, language string) (*models.
}
endpoint := fmt.Sprintf("%s/tv/%d/similar?%s", s.baseURL, id, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -450,7 +450,7 @@ func (s *TMDBService) GetSimilarTVShows(id, page int, language string) (*models.
func (s *TMDBService) GetMovieExternalIDs(id int) (*models.ExternalIDs, error) {
endpoint := fmt.Sprintf("%s/movie/%d/external_ids", s.baseURL, id)
var ids models.ExternalIDs
err := s.makeRequest(endpoint, &ids)
return &ids, err
@@ -458,7 +458,7 @@ func (s *TMDBService) GetMovieExternalIDs(id int) (*models.ExternalIDs, error) {
func (s *TMDBService) GetTVExternalIDs(id int) (*models.ExternalIDs, error) {
endpoint := fmt.Sprintf("%s/tv/%d/external_ids", s.baseURL, id)
var ids models.ExternalIDs
err := s.makeRequest(endpoint, &ids)
return &ids, err
@@ -469,7 +469,7 @@ func (s *TMDBService) DiscoverMoviesByGenre(genreID, page int, language string)
params.Set("page", strconv.Itoa(page))
params.Set("with_genres", strconv.Itoa(genreID))
params.Set("sort_by", "popularity.desc")
if language != "" {
params.Set("language", language)
} else {
@@ -477,7 +477,7 @@ func (s *TMDBService) DiscoverMoviesByGenre(genreID, page int, language string)
}
endpoint := fmt.Sprintf("%s/discover/movie?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -488,7 +488,7 @@ func (s *TMDBService) DiscoverTVByGenre(genreID, page int, language string) (*mo
params.Set("page", strconv.Itoa(page))
params.Set("with_genres", strconv.Itoa(genreID))
params.Set("sort_by", "popularity.desc")
if language != "" {
params.Set("language", language)
} else {
@@ -496,7 +496,7 @@ func (s *TMDBService) DiscoverTVByGenre(genreID, page int, language string) (*mo
}
endpoint := fmt.Sprintf("%s/discover/tv?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
@@ -508,8 +508,8 @@ func (s *TMDBService) GetTVSeason(tvID, seasonNumber int, language string) (*mod
}
endpoint := fmt.Sprintf("%s/tv/%d/season/%d?language=%s", s.baseURL, tvID, seasonNumber, language)
var season models.SeasonDetails
err := s.makeRequest(endpoint, &season)
return &season, err
}
}

View File

@@ -207,7 +207,6 @@ func (s *TorrentService) SearchTorrentsByIMDbID(tmdbService *TMDBService, imdbID
return response, nil
}
// SearchMovies - поиск фильмов с дополнительной фильтрацией
func (s *TorrentService) SearchMovies(title, originalTitle, year string) (*models.TorrentSearchResponse, error) {
params := map[string]string{
@@ -850,7 +849,7 @@ func (s *TorrentService) SearchByImdb(imdbID, contentType string, season *int) (
"is_serial": "2",
"category": "5000",
}
fallbackResp, err := s.SearchTorrents(paramsNoSeason)
if err == nil {
filtered := s.filterBySeason(fallbackResp.Results, *season)

View File

@@ -1,20 +1,23 @@
package services
import (
"fmt"
"go.mongodb.org/mongo-driver/mongo"
"neomovies-api/pkg/models"
)
type TVService struct {
db *mongo.Database
tmdb *TMDBService
db *mongo.Database
tmdb *TMDBService
kpService *KinopoiskService
}
func NewTVService(db *mongo.Database, tmdb *TMDBService) *TVService {
func NewTVService(db *mongo.Database, tmdb *TMDBService, kpService *KinopoiskService) *TVService {
return &TVService{
db: db,
tmdb: tmdb,
db: db,
tmdb: tmdb,
kpService: kpService,
}
}
@@ -22,7 +25,41 @@ func (s *TVService) Search(query string, page int, language string, year int) (*
return s.tmdb.SearchTVShows(query, page, language, year)
}
func (s *TVService) GetByID(id int, language string) (*models.TVShow, error) {
func (s *TVService) GetByID(id int, language string, idType string) (*models.TVShow, error) {
// Если указан id_type, используем его; иначе определяем по языку
useKP := false
if idType == "kp" {
useKP = true
} else if idType == "tmdb" {
useKP = false
} else {
// Если id_type не указан, используем старую логику по языку
useKP = ShouldUseKinopoisk(language)
}
if useKP && s.kpService != nil {
// Сначала пробуем напрямую по KP ID
kpFilm, err := s.kpService.GetFilmByKinopoiskId(id)
if err == nil && kpFilm != nil {
return MapKPFilmToTVShow(kpFilm), nil
}
// Если не найдено и явно указан id_type=kp, возможно это TMDB ID
// Пробуем конвертировать TMDB -> KP
if idType == "kp" {
kpId, convErr := TmdbIdToKPId(s.tmdb, s.kpService, id)
if convErr == nil {
kpFilm, err := s.kpService.GetFilmByKinopoiskId(kpId)
if err == nil && kpFilm != nil {
return MapKPFilmToTVShow(kpFilm), nil
}
}
// Если конвертация не удалась, возвращаем ошибку вместо fallback
return nil, fmt.Errorf("TV show not found in Kinopoisk with id %d", id)
}
}
// Для TMDB или если KP не указан
return s.tmdb.GetTVShow(id, language)
}
@@ -52,4 +89,4 @@ func (s *TVService) GetSimilar(id, page int, language string) (*models.TMDBTVRes
func (s *TVService) GetExternalIDs(id int) (*models.ExternalIDs, error) {
return s.tmdb.GetTVExternalIDs(id)
}
}