diff --git a/README.md b/README.md index d50eda5..bf6391d 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,87 @@ -# neomovies-api +# Neo Movies API +API для поиска фильмов и сериалов с поддержкой русского языка. +## Деплой на AlwaysData -## Getting started +1. Создайте аккаунт на [AlwaysData](https://www.alwaysdata.com) -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +2. Настройте SSH ключ: + ```bash + # Создайте SSH ключ если его нет + ssh-keygen -t rsa -b 4096 + + # Скопируйте публичный ключ + cat ~/.ssh/id_rsa.pub + ``` + Добавьте ключ в настройках AlwaysData (SSH Keys) -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +3. Подключитесь по SSH: + ```bash + # Замените username на ваш логин + ssh username@ssh-username.alwaysdata.net + ``` -## Add your files +4. Установите Go: + ```bash + # Создайте директорию для Go + mkdir -p $HOME/go/bin + + # Скачайте и установите Go + wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz + tar -C $HOME -xzf go1.21.5.linux-amd64.tar.gz + + # Добавьте Go в PATH + echo 'export PATH=$HOME/go/bin:$HOME/go/bin:$PATH' >> ~/.bashrc + source ~/.bashrc + ``` -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +5. Клонируйте репозиторий: + ```bash + git clone https://github.com/ваш-username/neomovies-api.git + cd neomovies-api + ``` -``` -cd existing_repo -git remote add origin https://gitlab.com/foxixus/neomovies-api.git -git branch -M main -git push -uf origin main +6. Соберите приложение: + ```bash + chmod +x build.sh + ./build.sh + ``` + +7. Настройте сервис в панели AlwaysData: + - Type: Site + - Name: neomovies-api + - Address: api.your-name.alwaysdata.net + - Command: $HOME/neomovies-api/run.sh + - Working directory: $HOME/neomovies-api + +8. Добавьте переменные окружения: + - `TMDB_ACCESS_TOKEN`: Ваш токен TMDB API + - `PORT`: 8080 (или порт по умолчанию) + +После деплоя ваше API будет доступно по адресу: https://api.your-name.alwaysdata.net + +## Локальная разработка + +1. Установите зависимости: +```bash +go mod download ``` -## Integrate with your tools +2. Запустите сервер: +```bash +go run main.go +``` -- [ ] [Set up project integrations](https://gitlab.com/foxixus/neomovies-api/-/settings/integrations) +API будет доступно по адресу: http://localhost:8080 -## Collaborate with your team +## API Endpoints -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +- `GET /movies/search` - Поиск фильмов +- `GET /movies/popular` - Популярные фильмы +- `GET /movies/top-rated` - Лучшие фильмы +- `GET /movies/upcoming` - Предстоящие фильмы +- `GET /movies/:id` - Информация о фильме +- `GET /health` - Проверка работоспособности API -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README - -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +Полная документация API доступна по адресу: `/swagger/index.html` diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..878f274 --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Переходим в директорию с приложением +cd "$HOME/neomovies-api" + +# Собираем приложение +go build -o app diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..0f5a7fd --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,806 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/bridge/tmdb/discover/movie": { + "get": { + "description": "Get a list of movies based on filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Discover movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/discover/tv": { + "get": { + "description": "Get a list of TV shows based on filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Discover TV shows", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/movie/popular": { + "get": { + "description": "Get a list of popular movies directly from TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB popular movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/movie/top_rated": { + "get": { + "description": "Get a list of top rated movies directly from TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB top rated movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/movie/upcoming": { + "get": { + "description": "Get a list of upcoming movies directly from TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB upcoming movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/movie/{id}": { + "get": { + "description": "Get detailed information about a specific movie directly from TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB movie details", + "parameters": [ + { + "type": "integer", + "description": "Movie ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.Movie" + } + } + } + } + }, + "/bridge/tmdb/movie/{id}/external_ids": { + "get": { + "description": "Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific movie", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB movie external IDs", + "parameters": [ + { + "type": "integer", + "description": "Movie ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.ExternalIDs" + } + } + } + } + }, + "/bridge/tmdb/search/movie": { + "get": { + "description": "Search for movies directly in TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Search TMDB movies", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.MoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/search/tv": { + "get": { + "description": "Search for TV shows directly in TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Search TMDB TV shows", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.TVSearchResults" + } + } + } + } + }, + "/bridge/tmdb/tv/{id}/external_ids": { + "get": { + "description": "Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific TV show", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB TV show external IDs", + "parameters": [ + { + "type": "integer", + "description": "TV Show ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.ExternalIDs" + } + } + } + } + }, + "/movies/popular": { + "get": { + "description": "Get a list of popular movies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Get popular movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MoviesResponse" + } + } + } + } + }, + "/movies/search": { + "get": { + "description": "Search for movies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Search movies", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MoviesResponse" + } + } + } + } + }, + "/movies/top-rated": { + "get": { + "description": "Get a list of top rated movies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Get top rated movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MoviesResponse" + } + } + } + } + }, + "/movies/upcoming": { + "get": { + "description": "Get a list of upcoming movies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Get upcoming movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MoviesResponse" + } + } + } + } + }, + "/movies/{id}": { + "get": { + "description": "Get detailed information about a specific movie", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Get movie details", + "parameters": [ + { + "type": "integer", + "description": "Movie ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MovieDetails" + } + } + } + } + } + }, + "definitions": { + "api.Genre": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "api.Movie": { + "type": "object", + "properties": { + "backdrop_path": { + "type": "string" + }, + "genres": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Genre" + } + }, + "id": { + "type": "integer" + }, + "overview": { + "type": "string" + }, + "poster_path": { + "type": "string" + }, + "release_date": { + "type": "string" + }, + "title": { + "type": "string" + }, + "vote_average": { + "type": "number" + } + } + }, + "api.MovieDetails": { + "type": "object", + "properties": { + "backdrop_path": { + "type": "string" + }, + "budget": { + "type": "integer" + }, + "genres": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Genre" + } + }, + "id": { + "type": "integer" + }, + "overview": { + "type": "string" + }, + "poster_path": { + "type": "string" + }, + "release_date": { + "type": "string" + }, + "revenue": { + "type": "integer" + }, + "runtime": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "tagline": { + "type": "string" + }, + "title": { + "type": "string" + }, + "vote_average": { + "type": "number" + } + } + }, + "api.MoviesResponse": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Movie" + } + }, + "total_pages": { + "type": "integer" + }, + "total_results": { + "type": "integer" + } + } + }, + "api.TMDBMoviesResponse": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Movie" + } + }, + "total_pages": { + "type": "integer" + }, + "total_results": { + "type": "integer" + } + } + }, + "tmdb.ExternalIDs": { + "type": "object", + "properties": { + "facebook_id": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "imdb_id": { + "type": "string" + }, + "instagram_id": { + "type": "string" + }, + "twitter_id": { + "type": "string" + } + } + }, + "tmdb.Genre": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "tmdb.Movie": { + "type": "object", + "properties": { + "backdrop_path": { + "type": "string" + }, + "genres": { + "type": "array", + "items": { + "$ref": "#/definitions/tmdb.Genre" + } + }, + "id": { + "type": "integer" + }, + "overview": { + "type": "string" + }, + "poster_path": { + "type": "string" + }, + "release_date": { + "type": "string" + }, + "title": { + "type": "string" + }, + "vote_average": { + "type": "number" + } + } + }, + "tmdb.MoviesResponse": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/tmdb.Movie" + } + }, + "total_pages": { + "type": "integer" + }, + "total_results": { + "type": "integer" + } + } + }, + "tmdb.TV": { + "type": "object", + "properties": { + "backdrop_path": { + "type": "string" + }, + "first_air_date": { + "type": "string" + }, + "genre_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "original_language": { + "type": "string" + }, + "original_name": { + "type": "string" + }, + "overview": { + "type": "string" + }, + "popularity": { + "type": "number" + }, + "poster_path": { + "type": "string" + }, + "vote_average": { + "type": "number" + }, + "vote_count": { + "type": "integer" + } + } + }, + "tmdb.TVSearchResults": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/tmdb.TV" + } + }, + "total_pages": { + "type": "integer" + }, + "total_results": { + "type": "integer" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/", + Schemes: []string{}, + Title: "Neo Movies API", + Description: "API для работы с фильмами", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..5879bc7 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,782 @@ +{ + "swagger": "2.0", + "info": { + "description": "API для работы с фильмами", + "title": "Neo Movies API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/", + "paths": { + "/bridge/tmdb/discover/movie": { + "get": { + "description": "Get a list of movies based on filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Discover movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/discover/tv": { + "get": { + "description": "Get a list of TV shows based on filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Discover TV shows", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/movie/popular": { + "get": { + "description": "Get a list of popular movies directly from TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB popular movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/movie/top_rated": { + "get": { + "description": "Get a list of top rated movies directly from TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB top rated movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/movie/upcoming": { + "get": { + "description": "Get a list of upcoming movies directly from TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB upcoming movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.TMDBMoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/movie/{id}": { + "get": { + "description": "Get detailed information about a specific movie directly from TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB movie details", + "parameters": [ + { + "type": "integer", + "description": "Movie ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.Movie" + } + } + } + } + }, + "/bridge/tmdb/movie/{id}/external_ids": { + "get": { + "description": "Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific movie", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB movie external IDs", + "parameters": [ + { + "type": "integer", + "description": "Movie ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.ExternalIDs" + } + } + } + } + }, + "/bridge/tmdb/search/movie": { + "get": { + "description": "Search for movies directly in TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Search TMDB movies", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.MoviesResponse" + } + } + } + } + }, + "/bridge/tmdb/search/tv": { + "get": { + "description": "Search for TV shows directly in TMDB", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Search TMDB TV shows", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.TVSearchResults" + } + } + } + } + }, + "/bridge/tmdb/tv/{id}/external_ids": { + "get": { + "description": "Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific TV show", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tmdb" + ], + "summary": "Get TMDB TV show external IDs", + "parameters": [ + { + "type": "integer", + "description": "TV Show ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tmdb.ExternalIDs" + } + } + } + } + }, + "/movies/popular": { + "get": { + "description": "Get a list of popular movies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Get popular movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MoviesResponse" + } + } + } + } + }, + "/movies/search": { + "get": { + "description": "Search for movies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Search movies", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MoviesResponse" + } + } + } + } + }, + "/movies/top-rated": { + "get": { + "description": "Get a list of top rated movies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Get top rated movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MoviesResponse" + } + } + } + } + }, + "/movies/upcoming": { + "get": { + "description": "Get a list of upcoming movies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Get upcoming movies", + "parameters": [ + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MoviesResponse" + } + } + } + } + }, + "/movies/{id}": { + "get": { + "description": "Get detailed information about a specific movie", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "movies" + ], + "summary": "Get movie details", + "parameters": [ + { + "type": "integer", + "description": "Movie ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MovieDetails" + } + } + } + } + } + }, + "definitions": { + "api.Genre": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "api.Movie": { + "type": "object", + "properties": { + "backdrop_path": { + "type": "string" + }, + "genres": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Genre" + } + }, + "id": { + "type": "integer" + }, + "overview": { + "type": "string" + }, + "poster_path": { + "type": "string" + }, + "release_date": { + "type": "string" + }, + "title": { + "type": "string" + }, + "vote_average": { + "type": "number" + } + } + }, + "api.MovieDetails": { + "type": "object", + "properties": { + "backdrop_path": { + "type": "string" + }, + "budget": { + "type": "integer" + }, + "genres": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Genre" + } + }, + "id": { + "type": "integer" + }, + "overview": { + "type": "string" + }, + "poster_path": { + "type": "string" + }, + "release_date": { + "type": "string" + }, + "revenue": { + "type": "integer" + }, + "runtime": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "tagline": { + "type": "string" + }, + "title": { + "type": "string" + }, + "vote_average": { + "type": "number" + } + } + }, + "api.MoviesResponse": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Movie" + } + }, + "total_pages": { + "type": "integer" + }, + "total_results": { + "type": "integer" + } + } + }, + "api.TMDBMoviesResponse": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Movie" + } + }, + "total_pages": { + "type": "integer" + }, + "total_results": { + "type": "integer" + } + } + }, + "tmdb.ExternalIDs": { + "type": "object", + "properties": { + "facebook_id": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "imdb_id": { + "type": "string" + }, + "instagram_id": { + "type": "string" + }, + "twitter_id": { + "type": "string" + } + } + }, + "tmdb.Genre": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "tmdb.Movie": { + "type": "object", + "properties": { + "backdrop_path": { + "type": "string" + }, + "genres": { + "type": "array", + "items": { + "$ref": "#/definitions/tmdb.Genre" + } + }, + "id": { + "type": "integer" + }, + "overview": { + "type": "string" + }, + "poster_path": { + "type": "string" + }, + "release_date": { + "type": "string" + }, + "title": { + "type": "string" + }, + "vote_average": { + "type": "number" + } + } + }, + "tmdb.MoviesResponse": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/tmdb.Movie" + } + }, + "total_pages": { + "type": "integer" + }, + "total_results": { + "type": "integer" + } + } + }, + "tmdb.TV": { + "type": "object", + "properties": { + "backdrop_path": { + "type": "string" + }, + "first_air_date": { + "type": "string" + }, + "genre_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "original_language": { + "type": "string" + }, + "original_name": { + "type": "string" + }, + "overview": { + "type": "string" + }, + "popularity": { + "type": "number" + }, + "poster_path": { + "type": "string" + }, + "vote_average": { + "type": "number" + }, + "vote_count": { + "type": "integer" + } + } + }, + "tmdb.TVSearchResults": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/tmdb.TV" + } + }, + "total_pages": { + "type": "integer" + }, + "total_results": { + "type": "integer" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..7900459 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,512 @@ +basePath: / +definitions: + api.Genre: + properties: + id: + type: integer + name: + type: string + type: object + api.Movie: + properties: + backdrop_path: + type: string + genres: + items: + $ref: '#/definitions/api.Genre' + type: array + id: + type: integer + overview: + type: string + poster_path: + type: string + release_date: + type: string + title: + type: string + vote_average: + type: number + type: object + api.MovieDetails: + properties: + backdrop_path: + type: string + budget: + type: integer + genres: + items: + $ref: '#/definitions/api.Genre' + type: array + id: + type: integer + overview: + type: string + poster_path: + type: string + release_date: + type: string + revenue: + type: integer + runtime: + type: integer + status: + type: string + tagline: + type: string + title: + type: string + vote_average: + type: number + type: object + api.MoviesResponse: + properties: + page: + type: integer + results: + items: + $ref: '#/definitions/api.Movie' + type: array + total_pages: + type: integer + total_results: + type: integer + type: object + api.TMDBMoviesResponse: + properties: + page: + type: integer + results: + items: + $ref: '#/definitions/api.Movie' + type: array + total_pages: + type: integer + total_results: + type: integer + type: object + tmdb.ExternalIDs: + properties: + facebook_id: + type: string + id: + type: integer + imdb_id: + type: string + instagram_id: + type: string + twitter_id: + type: string + type: object + tmdb.Genre: + properties: + id: + type: integer + name: + type: string + type: object + tmdb.Movie: + properties: + backdrop_path: + type: string + genres: + items: + $ref: '#/definitions/tmdb.Genre' + type: array + id: + type: integer + overview: + type: string + poster_path: + type: string + release_date: + type: string + title: + type: string + vote_average: + type: number + type: object + tmdb.MoviesResponse: + properties: + page: + type: integer + results: + items: + $ref: '#/definitions/tmdb.Movie' + type: array + total_pages: + type: integer + total_results: + type: integer + type: object + tmdb.TV: + properties: + backdrop_path: + type: string + first_air_date: + type: string + genre_ids: + items: + type: integer + type: array + id: + type: integer + name: + type: string + original_language: + type: string + original_name: + type: string + overview: + type: string + popularity: + type: number + poster_path: + type: string + vote_average: + type: number + vote_count: + type: integer + type: object + tmdb.TVSearchResults: + properties: + page: + type: integer + results: + items: + $ref: '#/definitions/tmdb.TV' + type: array + total_pages: + type: integer + total_results: + type: integer + type: object +host: localhost:8080 +info: + contact: {} + description: API для работы с фильмами + title: Neo Movies API + version: "1.0" +paths: + /bridge/tmdb/discover/movie: + get: + consumes: + - application/json + description: Get a list of movies based on filters + parameters: + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.TMDBMoviesResponse' + summary: Discover movies + tags: + - tmdb + /bridge/tmdb/discover/tv: + get: + consumes: + - application/json + description: Get a list of TV shows based on filters + parameters: + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.TMDBMoviesResponse' + summary: Discover TV shows + tags: + - tmdb + /bridge/tmdb/movie/{id}: + get: + consumes: + - application/json + description: Get detailed information about a specific movie directly from TMDB + parameters: + - description: Movie ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tmdb.Movie' + summary: Get TMDB movie details + tags: + - tmdb + /bridge/tmdb/movie/{id}/external_ids: + get: + consumes: + - application/json + description: Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific + movie + parameters: + - description: Movie ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tmdb.ExternalIDs' + summary: Get TMDB movie external IDs + tags: + - tmdb + /bridge/tmdb/movie/popular: + get: + consumes: + - application/json + description: Get a list of popular movies directly from TMDB + parameters: + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.TMDBMoviesResponse' + summary: Get TMDB popular movies + tags: + - tmdb + /bridge/tmdb/movie/top_rated: + get: + consumes: + - application/json + description: Get a list of top rated movies directly from TMDB + parameters: + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.TMDBMoviesResponse' + summary: Get TMDB top rated movies + tags: + - tmdb + /bridge/tmdb/movie/upcoming: + get: + consumes: + - application/json + description: Get a list of upcoming movies directly from TMDB + parameters: + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.TMDBMoviesResponse' + summary: Get TMDB upcoming movies + tags: + - tmdb + /bridge/tmdb/search/movie: + get: + consumes: + - application/json + description: Search for movies directly in TMDB + parameters: + - description: Search query + in: query + name: query + required: true + type: string + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tmdb.MoviesResponse' + summary: Search TMDB movies + tags: + - tmdb + /bridge/tmdb/search/tv: + get: + consumes: + - application/json + description: Search for TV shows directly in TMDB + parameters: + - description: Search query + in: query + name: query + required: true + type: string + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tmdb.TVSearchResults' + summary: Search TMDB TV shows + tags: + - tmdb + /bridge/tmdb/tv/{id}/external_ids: + get: + consumes: + - application/json + description: Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific + TV show + parameters: + - description: TV Show ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tmdb.ExternalIDs' + summary: Get TMDB TV show external IDs + tags: + - tmdb + /movies/{id}: + get: + consumes: + - application/json + description: Get detailed information about a specific movie + parameters: + - description: Movie ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MovieDetails' + summary: Get movie details + tags: + - movies + /movies/popular: + get: + consumes: + - application/json + description: Get a list of popular movies + parameters: + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MoviesResponse' + summary: Get popular movies + tags: + - movies + /movies/search: + get: + consumes: + - application/json + description: Search for movies + parameters: + - description: Search query + in: query + name: query + required: true + type: string + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MoviesResponse' + summary: Search movies + tags: + - movies + /movies/top-rated: + get: + consumes: + - application/json + description: Get a list of top rated movies + parameters: + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MoviesResponse' + summary: Get top rated movies + tags: + - movies + /movies/upcoming: + get: + consumes: + - application/json + description: Get a list of upcoming movies + parameters: + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.MoviesResponse' + summary: Get upcoming movies + tags: + - movies +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..547a6bd --- /dev/null +++ b/go.mod @@ -0,0 +1,55 @@ +module neomovies-api + +go 1.21.0 + +toolchain go1.23.4 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/joho/godotenv v1.5.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.2 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/bytedance/sonic v1.12.6 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/gin-contrib/cors v1.7.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/protobuf v1.36.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + h12.io/socks v1.0.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4db1636 --- /dev/null +++ b/go.sum @@ -0,0 +1,156 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= +github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns= +github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= +h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/internal/api/handlers.go b/internal/api/handlers.go new file mode 100644 index 0000000..ed09ac1 --- /dev/null +++ b/internal/api/handlers.go @@ -0,0 +1,505 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "neomovies-api/internal/tmdb" +) + +// GetPopularMovies возвращает список популярных фильмов +// @Summary Get popular movies +// @Description Get a list of popular movies +// @Tags movies +// @Accept json +// @Produce json +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} MoviesResponse +// @Router /movies/popular [get] +func GetPopularMovies(c *gin.Context) { + page := c.DefaultQuery("page", "1") + + movies, err := tmdbClient.GetPopular(page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Добавляем полные URL для изображений + for i := range movies.Results { + if movies.Results[i].PosterPath != "" { + movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500") + } + if movies.Results[i].BackdropPath != "" { + movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280") + } + } + + c.JSON(http.StatusOK, movies) +} + +// GetMovie возвращает информацию о фильме +// @Summary Get movie details +// @Description Get detailed information about a specific movie +// @Tags movies +// @Accept json +// @Produce json +// @Param id path int true "Movie ID" +// @Success 200 {object} MovieDetails +// @Router /movies/{id} [get] +func GetMovie(c *gin.Context) { + id := c.Param("id") + + movie, err := tmdbClient.GetMovie(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Добавляем полные URL для изображений + if movie.PosterPath != "" { + movie.PosterPath = tmdbClient.GetImageURL(movie.PosterPath, "original") + } + if movie.BackdropPath != "" { + movie.BackdropPath = tmdbClient.GetImageURL(movie.BackdropPath, "original") + } + + // Обрабатываем изображения для коллекции + if movie.BelongsToCollection != nil { + if movie.BelongsToCollection.PosterPath != "" { + movie.BelongsToCollection.PosterPath = tmdbClient.GetImageURL(movie.BelongsToCollection.PosterPath, "w500") + } + if movie.BelongsToCollection.BackdropPath != "" { + movie.BelongsToCollection.BackdropPath = tmdbClient.GetImageURL(movie.BelongsToCollection.BackdropPath, "w1280") + } + } + + // Обрабатываем логотипы компаний + for i := range movie.ProductionCompanies { + if movie.ProductionCompanies[i].LogoPath != "" { + movie.ProductionCompanies[i].LogoPath = tmdbClient.GetImageURL(movie.ProductionCompanies[i].LogoPath, "w185") + } + } + + c.JSON(http.StatusOK, movie) +} + +// SearchMovies ищет фильмы +// @Summary Поиск фильмов +// @Description Поиск фильмов по запросу +// @Tags movies +// @Accept json +// @Produce json +// @Param query query string true "Поисковый запрос" +// @Param page query string false "Номер страницы (по умолчанию 1)" +// @Success 200 {object} SearchResponse +// @Router /movies/search [get] +func SearchMovies(c *gin.Context) { + query := c.Query("query") + if query == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter is required"}) + return + } + + page := c.DefaultQuery("page", "1") + + // Получаем результаты поиска + results, err := tmdbClient.SearchMovies(query, page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Преобразуем результаты в формат ответа + response := SearchResponse{ + Page: results.Page, + TotalPages: results.TotalPages, + TotalResults: results.TotalResults, + Results: make([]MovieResponse, 0), + } + + // Преобразуем каждый фильм + for _, movie := range results.Results { + // Форматируем дату + releaseDate := formatDate(movie.ReleaseDate) + + // Добавляем фильм в результаты + response.Results = append(response.Results, MovieResponse{ + ID: movie.ID, + Title: movie.Title, + Overview: movie.Overview, + ReleaseDate: releaseDate, + VoteAverage: movie.VoteAverage, + PosterPath: tmdbClient.GetImageURL(movie.PosterPath, "w500"), + BackdropPath: tmdbClient.GetImageURL(movie.BackdropPath, "w1280"), + }) + } + + c.JSON(http.StatusOK, response) +} + +// GetTopRatedMovies возвращает список лучших фильмов +// @Summary Get top rated movies +// @Description Get a list of top rated movies +// @Tags movies +// @Accept json +// @Produce json +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} MoviesResponse +// @Router /movies/top-rated [get] +func GetTopRatedMovies(c *gin.Context) { + page := c.DefaultQuery("page", "1") + + movies, err := tmdbClient.GetTopRated(page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Добавляем полные URL для изображений + for i := range movies.Results { + if movies.Results[i].PosterPath != "" { + movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500") + } + if movies.Results[i].BackdropPath != "" { + movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280") + } + } + + c.JSON(http.StatusOK, movies) +} + +// GetUpcomingMovies возвращает список предстоящих фильмов +// @Summary Get upcoming movies +// @Description Get a list of upcoming movies +// @Tags movies +// @Accept json +// @Produce json +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} MoviesResponse +// @Router /movies/upcoming [get] +func GetUpcomingMovies(c *gin.Context) { + page := c.DefaultQuery("page", "1") + + movies, err := tmdbClient.GetUpcoming(page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Добавляем полные URL для изображений + for i := range movies.Results { + if movies.Results[i].PosterPath != "" { + movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500") + } + if movies.Results[i].BackdropPath != "" { + movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280") + } + } + + c.JSON(http.StatusOK, movies) +} + +// GetTMDBPopularMovies возвращает список популярных фильмов из TMDB +// @Summary Get TMDB popular movies +// @Description Get a list of popular movies directly from TMDB +// @Tags tmdb +// @Accept json +// @Produce json +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} TMDBMoviesResponse +// @Router /bridge/tmdb/movie/popular [get] +func GetTMDBPopularMovies(c *gin.Context) { + page := c.DefaultQuery("page", "1") + + movies, err := tmdbClient.GetPopular(page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Добавляем полные URL для изображений + for i := range movies.Results { + if movies.Results[i].PosterPath != "" { + movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500") + } + if movies.Results[i].BackdropPath != "" { + movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280") + } + } + + c.JSON(http.StatusOK, movies) +} + +// GetTMDBMovie возвращает информацию о фильме из TMDB +// @Summary Get TMDB movie details +// @Description Get detailed information about a specific movie directly from TMDB +// @Tags tmdb +// @Accept json +// @Produce json +// @Param id path int true "Movie ID" +// @Success 200 {object} tmdb.Movie +// @Router /bridge/tmdb/movie/{id} [get] +func GetTMDBMovie(c *gin.Context) { + id := c.Param("id") + + movie, err := tmdbClient.GetMovie(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, movie) +} + +// GetTMDBTopRatedMovies возвращает список лучших фильмов из TMDB +// @Summary Get TMDB top rated movies +// @Description Get a list of top rated movies directly from TMDB +// @Tags tmdb +// @Accept json +// @Produce json +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} TMDBMoviesResponse +// @Router /bridge/tmdb/movie/top_rated [get] +func GetTMDBTopRatedMovies(c *gin.Context) { + page := c.DefaultQuery("page", "1") + + movies, err := tmdbClient.GetTopRated(page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Добавляем полные URL для изображений + for i := range movies.Results { + if movies.Results[i].PosterPath != "" { + movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500") + } + if movies.Results[i].BackdropPath != "" { + movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280") + } + } + + c.JSON(http.StatusOK, movies) +} + +// GetTMDBUpcomingMovies возвращает список предстоящих фильмов из TMDB +// @Summary Get TMDB upcoming movies +// @Description Get a list of upcoming movies directly from TMDB +// @Tags tmdb +// @Accept json +// @Produce json +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} TMDBMoviesResponse +// @Router /bridge/tmdb/movie/upcoming [get] +func GetTMDBUpcomingMovies(c *gin.Context) { + page := c.DefaultQuery("page", "1") + + movies, err := tmdbClient.GetUpcoming(page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Добавляем полные URL для изображений + for i := range movies.Results { + if movies.Results[i].PosterPath != "" { + movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500") + } + if movies.Results[i].BackdropPath != "" { + movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280") + } + } + + c.JSON(http.StatusOK, movies) +} + +// SearchTMDBMovies ищет фильмы в TMDB +// @Summary Search TMDB movies +// @Description Search for movies directly in TMDB +// @Tags tmdb +// @Accept json +// @Produce json +// @Param query query string true "Search query" +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} tmdb.MoviesResponse +// @Router /bridge/tmdb/search/movie [get] +func SearchTMDBMovies(c *gin.Context) { + query := c.Query("query") + if query == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter is required"}) + return + } + + page := c.DefaultQuery("page", "1") + movies, err := tmdbClient.SearchMovies(query, page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, movies) +} + +// SearchTMDBTV ищет сериалы в TMDB +// @Summary Search TMDB TV shows +// @Description Search for TV shows directly in TMDB +// @Tags tmdb +// @Accept json +// @Produce json +// @Param query query string true "Search query" +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} tmdb.TVSearchResults +// @Router /bridge/tmdb/search/tv [get] +func SearchTMDBTV(c *gin.Context) { + query := c.Query("query") + if query == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter is required"}) + return + } + + page := c.DefaultQuery("page", "1") + tv, err := tmdbClient.SearchTV(query, page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, tv) +} + +// DiscoverMovies возвращает список фильмов по фильтрам +// @Summary Discover movies +// @Description Get a list of movies based on filters +// @Tags tmdb +// @Accept json +// @Produce json +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} TMDBMoviesResponse +// @Router /bridge/tmdb/discover/movie [get] +func DiscoverMovies(c *gin.Context) { + page := c.DefaultQuery("page", "1") + movies, err := tmdbClient.DiscoverMovies(page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, movies) +} + +// DiscoverTV возвращает список сериалов по фильтрам +// @Summary Discover TV shows +// @Description Get a list of TV shows based on filters +// @Tags tmdb +// @Accept json +// @Produce json +// @Param page query int false "Page number (default: 1)" +// @Success 200 {object} TMDBMoviesResponse +// @Router /bridge/tmdb/discover/tv [get] +func DiscoverTV(c *gin.Context) { + page := c.DefaultQuery("page", "1") + shows, err := tmdbClient.DiscoverTV(page) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, shows) +} + +// GetTMDBMovieExternalIDs возвращает внешние идентификаторы фильма +// @Summary Get TMDB movie external IDs +// @Description Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific movie +// @Tags tmdb +// @Accept json +// @Produce json +// @Param id path int true "Movie ID" +// @Success 200 {object} tmdb.ExternalIDs +// @Router /bridge/tmdb/movie/{id}/external_ids [get] +func GetTMDBMovieExternalIDs(c *gin.Context) { + id := c.Param("id") + + externalIDs, err := tmdbClient.GetMovieExternalIDs(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, externalIDs) +} + +// GetTMDBTVExternalIDs возвращает внешние идентификаторы сериала +// @Summary Get TMDB TV show external IDs +// @Description Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific TV show +// @Tags tmdb +// @Accept json +// @Produce json +// @Param id path int true "TV Show ID" +// @Success 200 {object} tmdb.ExternalIDs +// @Router /bridge/tmdb/tv/{id}/external_ids [get] +func GetTMDBTVExternalIDs(c *gin.Context) { + id := c.Param("id") + + externalIDs, err := tmdbClient.GetTVExternalIDs(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, externalIDs) +} + +// HealthCheck godoc +// @Summary Проверка работоспособности API +// @Description Проверяет, что API работает +// @Tags health +// @Produce json +// @Success 200 {object} map[string]string +// @Router /health [get] +func HealthCheck(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "ok", + }) +} + +// InitTMDBClientWithProxy инициализирует TMDB клиент с прокси +func InitTMDBClientWithProxy(apiKey string, proxyAddr string) error { + tmdbClient = tmdb.NewClient(apiKey) + return tmdbClient.SetSOCKS5Proxy(proxyAddr) +} + +// Admin handlers + +// GetAdminMovies возвращает список фильмов для админа +func GetAdminMovies(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Admin movies list"}) +} + +// ToggleMovieVisibility переключает видимость фильма +func ToggleMovieVisibility(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Movie visibility toggled"}) +} + +// GetUsers возвращает список пользователей +func GetUsers(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Users list"}) +} + +// CreateUser создает нового пользователя +func CreateUser(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "User created"}) +} + +// ToggleAdmin переключает права администратора +func ToggleAdmin(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Admin status toggled"}) +} + +// SendVerification отправляет код верификации +func SendVerification(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Verification code sent"}) +} + +// VerifyCode проверяет код верификации +func VerifyCode(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Code verified"}) +} diff --git a/internal/api/init.go b/internal/api/init.go new file mode 100644 index 0000000..874cf66 --- /dev/null +++ b/internal/api/init.go @@ -0,0 +1,14 @@ +package api + +import ( + "neomovies-api/internal/tmdb" +) + +var ( + tmdbClient *tmdb.Client +) + +// InitTMDBClient инициализирует TMDB клиент +func InitTMDBClient(apiKey string) { + tmdbClient = tmdb.NewClient(apiKey) +} diff --git a/internal/api/models.go b/internal/api/models.go new file mode 100644 index 0000000..42d6eb0 --- /dev/null +++ b/internal/api/models.go @@ -0,0 +1,64 @@ +package api + +// Genre представляет жанр фильма +type Genre struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// Movie представляет базовую информацию о фильме +type Movie struct { + ID int `json:"id"` + Title string `json:"title"` + Overview string `json:"overview"` + PosterPath *string `json:"poster_path"` + BackdropPath *string `json:"backdrop_path"` + ReleaseDate string `json:"release_date"` + VoteAverage float64 `json:"vote_average"` + Genres []Genre `json:"genres"` +} + +// MovieDetails представляет детальную информацию о фильме +type MovieDetails struct { + Movie + Runtime int `json:"runtime"` + Tagline string `json:"tagline"` + Budget int `json:"budget"` + Revenue int `json:"revenue"` + Status string `json:"status"` +} + +// MoviesResponse представляет ответ со списком фильмов +type MoviesResponse struct { + Page int `json:"page"` + TotalPages int `json:"total_pages"` + TotalResults int `json:"total_results"` + Results []Movie `json:"results"` +} + +// TMDBMoviesResponse представляет ответ со списком фильмов от TMDB API +type TMDBMoviesResponse struct { + Page int `json:"page"` + TotalPages int `json:"total_pages"` + TotalResults int `json:"total_results"` + Results []Movie `json:"results"` +} + +// SearchResponse представляет ответ на поисковый запрос +type SearchResponse struct { + Page int `json:"page"` + TotalPages int `json:"total_pages"` + TotalResults int `json:"total_results"` + Results []MovieResponse `json:"results"` +} + +// MovieResponse представляет информацию о фильме в ответе API +type MovieResponse struct { + ID int `json:"id"` + Title string `json:"title"` + Overview string `json:"overview"` + ReleaseDate string `json:"release_date"` + VoteAverage float64 `json:"vote_average"` + PosterPath string `json:"poster_path"` + BackdropPath string `json:"backdrop_path"` +} diff --git a/internal/api/utils.go b/internal/api/utils.go new file mode 100644 index 0000000..3b62c43 --- /dev/null +++ b/internal/api/utils.go @@ -0,0 +1,24 @@ +package api + +import "time" + +// formatDate форматирует дату в более читаемый формат +func formatDate(date string) string { + if date == "" { + return "" + } + + // Парсим дату из формата YYYY-MM-DD + t, err := time.Parse("2006-01-02", date) + if err != nil { + return date + } + + // Форматируем дату в русском стиле + months := []string{ + "января", "февраля", "марта", "апреля", "мая", "июня", + "июля", "августа", "сентября", "октября", "ноября", "декабря", + } + + return t.Format("2") + " " + months[t.Month()-1] + " " + t.Format("2006") +} diff --git a/internal/tmdb/client.go b/internal/tmdb/client.go new file mode 100644 index 0000000..582849e --- /dev/null +++ b/internal/tmdb/client.go @@ -0,0 +1,399 @@ +package tmdb + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "net/url" + "path" + "time" +) + +const ( + baseURL = "https://api.themoviedb.org/3" + imageBaseURL = "https://image.tmdb.org/t/p" + googleDNS = "8.8.8.8:53" // Google Public DNS + cloudflareDNS = "1.1.1.1:53" // Cloudflare DNS +) + +// Client представляет клиент для работы с TMDB API +type Client struct { + apiKey string + httpClient *http.Client +} + +// NewClient создает новый клиент TMDB API с кастомным DNS +func NewClient(apiKey string) *Client { + // Создаем кастомный DNS резолвер с двумя DNS серверами + dialer := &net.Dialer{ + Timeout: 5 * time.Second, + Resolver: &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + // Пробуем сначала Google DNS + d := net.Dialer{Timeout: 5 * time.Second} + conn, err := d.DialContext(ctx, "udp", googleDNS) + if err != nil { + log.Printf("Failed to connect to Google DNS, trying Cloudflare: %v", err) + // Если Google DNS не отвечает, пробуем Cloudflare + return d.DialContext(ctx, "udp", cloudflareDNS) + } + return conn, nil + }, + }, + } + + // Создаем транспорт с кастомным диалером + transport := &http.Transport{ + DialContext: dialer.DialContext, + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + } + + client := &Client{ + apiKey: apiKey, + httpClient: &http.Client{ + Transport: transport, + Timeout: 10 * time.Second, + }, + } + + // Проверяем работу DNS и API + log.Println("Testing DNS resolution and TMDB API access...") + + // Тест 1: Проверяем резолвинг через DNS + ips, err := net.LookupIP("api.themoviedb.org") + if err != nil { + log.Printf("Warning: DNS lookup failed: %v", err) + } else { + log.Printf("Successfully resolved api.themoviedb.org to: %v", ips) + } + + // Тест 2: Проверяем наш IP + resp, err := client.httpClient.Get("https://ipinfo.io/json") + if err != nil { + log.Printf("Warning: Failed to check our IP: %v", err) + } else { + defer resp.Body.Close() + var ipInfo struct { + IP string `json:"ip"` + City string `json:"city"` + Country string `json:"country"` + Org string `json:"org"` + } + if err := json.NewDecoder(resp.Body).Decode(&ipInfo); err != nil { + log.Printf("Warning: Failed to decode IP info: %v", err) + } else { + log.Printf("Our IP info: IP=%s, City=%s, Country=%s, Org=%s", + ipInfo.IP, ipInfo.City, ipInfo.Country, ipInfo.Org) + } + } + + // Тест 3: Проверяем доступ к TMDB API + testURL := fmt.Sprintf("%s/movie/popular?api_key=%s", baseURL, apiKey) + resp, err = client.httpClient.Get(testURL) + if err != nil { + log.Printf("Warning: TMDB API test failed: %v", err) + } else { + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + log.Println("Successfully connected to TMDB API!") + } else { + log.Printf("Warning: TMDB API returned status code: %d", resp.StatusCode) + } + } + + return client +} + +// SetSOCKS5Proxy устанавливает SOCKS5 прокси для клиента +func (c *Client) SetSOCKS5Proxy(proxyAddr string) error { + return fmt.Errorf("proxy support has been removed in favor of custom DNS resolvers") +} + +// makeRequest выполняет HTTP запрос к TMDB API +func (c *Client) makeRequest(method, endpoint string, params url.Values) ([]byte, error) { + // Создаем URL + u, err := url.Parse(baseURL) + if err != nil { + return nil, fmt.Errorf("failed to parse base URL: %v", err) + } + u.Path = path.Join(u.Path, endpoint) + if params == nil { + params = url.Values{} + } + u.RawQuery = params.Encode() + + // Создаем запрос + req, err := http.NewRequest(method, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + // Добавляем заголовок авторизации + req.Header.Set("Authorization", "Bearer "+c.apiKey) + req.Header.Set("Content-Type", "application/json;charset=utf-8") + + log.Printf("Making request to TMDB: %s %s", method, u.String()) + + // Выполняем запрос + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + defer resp.Body.Close() + + // Проверяем статус ответа + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("TMDB API error: status=%d body=%s", resp.StatusCode, string(body)) + } + + // Читаем тело ответа + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + + return body, nil +} + +// GetImageURL возвращает полный URL изображения +func (c *Client) GetImageURL(path string, size string) string { + if path == "" { + return "" + } + return fmt.Sprintf("%s/%s%s", imageBaseURL, size, path) +} + +// GetPopular получает список популярных фильмов +func (c *Client) GetPopular(page string) (*MoviesResponse, error) { + params := url.Values{} + params.Set("page", page) + + body, err := c.makeRequest(http.MethodGet, "movie/popular", params) + if err != nil { + return nil, err + } + + var response MoviesResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &response, nil +} + +// GetMovie получает информацию о конкретном фильме +func (c *Client) GetMovie(id string) (*MovieDetails, error) { + body, err := c.makeRequest(http.MethodGet, fmt.Sprintf("movie/%s", id), nil) + if err != nil { + return nil, err + } + + var movie MovieDetails + if err := json.Unmarshal(body, &movie); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &movie, nil +} + +// SearchMovies ищет фильмы по запросу с поддержкой русского языка +func (c *Client) SearchMovies(query string, page string) (*MoviesResponse, error) { + params := url.Values{} + params.Set("query", query) + params.Set("page", page) + params.Set("language", "ru-RU") // Добавляем русский язык + params.Set("region", "RU") // Добавляем русский регион + params.Set("include_adult", "false") // Исключаем взрослый контент + + body, err := c.makeRequest(http.MethodGet, "search/movie", params) + if err != nil { + return nil, err + } + + var response MoviesResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + // Фильтруем результаты + filteredResults := make([]Movie, 0) + for _, movie := range response.Results { + // Проверяем, что у фильма есть постер и описание + if movie.PosterPath != "" && movie.Overview != "" { + // Проверяем, что рейтинг больше 0 + if movie.VoteAverage > 0 { + filteredResults = append(filteredResults, movie) + } + } + } + + // Обновляем результаты + response.Results = filteredResults + response.TotalResults = len(filteredResults) + + return &response, nil +} + +// GetTopRated получает список лучших фильмов +func (c *Client) GetTopRated(page string) (*MoviesResponse, error) { + params := url.Values{} + params.Set("page", page) + + body, err := c.makeRequest(http.MethodGet, "movie/top_rated", params) + if err != nil { + return nil, err + } + + var response MoviesResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &response, nil +} + +// GetUpcoming получает список предстоящих фильмов +func (c *Client) GetUpcoming(page string) (*MoviesResponse, error) { + params := url.Values{} + params.Set("page", page) + + body, err := c.makeRequest(http.MethodGet, "movie/upcoming", params) + if err != nil { + return nil, err + } + + var response MoviesResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &response, nil +} + +// DiscoverMovies получает список фильмов по фильтрам +func (c *Client) DiscoverMovies(page string) (*MoviesResponse, error) { + params := url.Values{} + params.Set("page", page) + + body, err := c.makeRequest(http.MethodGet, "discover/movie", params) + if err != nil { + return nil, err + } + + var response MoviesResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &response, nil +} + +// DiscoverTV получает список сериалов по фильтрам +func (c *Client) DiscoverTV(page string) (*MoviesResponse, error) { + params := url.Values{} + params.Set("page", page) + + body, err := c.makeRequest(http.MethodGet, "discover/tv", params) + if err != nil { + return nil, err + } + + var response MoviesResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &response, nil +} + +// ExternalIDs содержит внешние идентификаторы фильма/сериала +type ExternalIDs struct { + ID int `json:"id"` + IMDbID string `json:"imdb_id"` + FacebookID string `json:"facebook_id"` + InstagramID string `json:"instagram_id"` + TwitterID string `json:"twitter_id"` +} + +// GetMovieExternalIDs возвращает внешние идентификаторы фильма +func (c *Client) GetMovieExternalIDs(id string) (*ExternalIDs, error) { + body, err := c.makeRequest(http.MethodGet, fmt.Sprintf("movie/%s/external_ids", id), nil) + if err != nil { + return nil, err + } + + var externalIDs ExternalIDs + if err := json.Unmarshal(body, &externalIDs); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &externalIDs, nil +} + +// GetTVExternalIDs возвращает внешние идентификаторы сериала +func (c *Client) GetTVExternalIDs(id string) (*ExternalIDs, error) { + body, err := c.makeRequest(http.MethodGet, fmt.Sprintf("tv/%s/external_ids", id), nil) + if err != nil { + return nil, err + } + + var externalIDs ExternalIDs + if err := json.Unmarshal(body, &externalIDs); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &externalIDs, nil +} + +// TVSearchResults содержит результаты поиска сериалов +type TVSearchResults struct { + Page int `json:"page"` + TotalResults int `json:"total_results"` + TotalPages int `json:"total_pages"` + Results []TV `json:"results"` +} + +// TV содержит информацию о сериале +type TV struct { + ID int `json:"id"` + Name string `json:"name"` + OriginalName string `json:"original_name"` + Overview string `json:"overview"` + FirstAirDate string `json:"first_air_date"` + PosterPath string `json:"poster_path"` + BackdropPath string `json:"backdrop_path"` + VoteAverage float64 `json:"vote_average"` + VoteCount int `json:"vote_count"` + Popularity float64 `json:"popularity"` + OriginalLanguage string `json:"original_language"` + GenreIDs []int `json:"genre_ids"` +} + +// SearchTV ищет сериалы в TMDB +func (c *Client) SearchTV(query string, page string) (*TVSearchResults, error) { + params := url.Values{} + params.Set("query", query) + params.Set("page", page) + + body, err := c.makeRequest(http.MethodGet, "search/tv", params) + if err != nil { + return nil, err + } + + var results TVSearchResults + if err := json.Unmarshal(body, &results); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &results, nil +} diff --git a/internal/tmdb/models.go b/internal/tmdb/models.go new file mode 100644 index 0000000..28bc434 --- /dev/null +++ b/internal/tmdb/models.go @@ -0,0 +1,76 @@ +package tmdb + +// MoviesResponse представляет ответ от TMDB API со списком фильмов +type MoviesResponse struct { + Page int `json:"page"` + Results []Movie `json:"results"` + TotalPages int `json:"total_pages"` + TotalResults int `json:"total_results"` +} + +// Movie представляет информацию о фильме +type Movie struct { + Adult bool `json:"adult"` + BackdropPath string `json:"backdrop_path"` + GenreIDs []int `json:"genre_ids"` + ID int `json:"id"` + OriginalLanguage string `json:"original_language"` + OriginalTitle string `json:"original_title"` + Overview string `json:"overview"` + Popularity float64 `json:"popularity"` + PosterPath string `json:"poster_path"` + ReleaseDate string `json:"release_date"` + Title string `json:"title"` + Video bool `json:"video"` + VoteAverage float64 `json:"vote_average"` + VoteCount int `json:"vote_count"` +} + +// Genre представляет жанр фильма +type Genre struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// Collection представляет коллекцию фильмов +type Collection struct { + ID int `json:"id"` + Name string `json:"name"` + PosterPath string `json:"poster_path"` + BackdropPath string `json:"backdrop_path"` +} + +// ProductionCompany представляет компанию-производителя +type ProductionCompany struct { + ID int `json:"id"` + LogoPath string `json:"logo_path"` + Name string `json:"name"` + Country string `json:"origin_country"` +} + +// MovieDetails представляет детальную информацию о фильме +type MovieDetails struct { + Adult bool `json:"adult"` + BackdropPath string `json:"backdrop_path"` + BelongsToCollection *Collection `json:"belongs_to_collection"` + Budget int `json:"budget"` + Genres []Genre `json:"genres"` + Homepage string `json:"homepage"` + ID int `json:"id"` + IMDbID string `json:"imdb_id"` + OriginalLanguage string `json:"original_language"` + OriginalTitle string `json:"original_title"` + Overview string `json:"overview"` + Popularity float64 `json:"popularity"` + PosterPath string `json:"poster_path"` + ProductionCompanies []ProductionCompany `json:"production_companies"` + ReleaseDate string `json:"release_date"` + Revenue int `json:"revenue"` + Runtime int `json:"runtime"` + Status string `json:"status"` + Tagline string `json:"tagline"` + Title string `json:"title"` + Video bool `json:"video"` + VoteAverage float64 `json:"vote_average"` + VoteCount int `json:"vote_count"` +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ef26ad0 --- /dev/null +++ b/main.go @@ -0,0 +1,125 @@ +package main + +import ( + "log" + "os" + + "neomovies-api/internal/api" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + + _ "neomovies-api/docs" +) + +// @title Neo Movies API +// @version 1.0 +// @description API для работы с фильмами +// @host localhost:8080 +// @BasePath / +func main() { + // Устанавливаем переменные окружения + os.Setenv("GIN_MODE", "debug") + os.Setenv("PORT", "8080") + os.Setenv("TMDB_ACCESS_TOKEN", "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI4ZmU3ODhlYmI5ZDAwNjZiNjQ2MWZhNzk5M2MyMzcxYiIsIm5iZiI6MTcyMzQwMTM3My4yMDgsInN1YiI6IjY2YjkwNDlkNzU4ZDQxOTQwYzA3NjlhNSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.x50tvcWDdBTEhtwRb3dE7aEe9qu4sXV_qOjLMn_Vmew") + + // Инициализируем TMDB клиент с CommsOne DNS + log.Println("Initializing TMDB client with CommsOne DNS") + api.InitTMDBClient(os.Getenv("TMDB_ACCESS_TOKEN")) + + // Устанавливаем режим Gin + gin.SetMode(os.Getenv("GIN_MODE")) + + // Создаем роутер + r := gin.Default() + + // Настраиваем CORS + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + })) + + // Swagger документация + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + // Health check + r.GET("/health", api.HealthCheck) + + // Movies API + movies := r.Group("/movies") + { + movies.GET("/popular", api.GetPopularMovies) + movies.GET("/search", api.SearchMovies) + movies.GET("/top-rated", api.GetTopRatedMovies) + movies.GET("/upcoming", api.GetUpcomingMovies) + movies.GET("/:id", api.GetMovie) + } + + // Bridge API + bridge := r.Group("/bridge") + { + // TMDB endpoints + tmdb := bridge.Group("/tmdb") + { + // Movie endpoints + movie := tmdb.Group("/movie") + { + movie.GET("/popular", api.GetTMDBPopularMovies) + movie.GET("/top_rated", api.GetTMDBTopRatedMovies) + movie.GET("/upcoming", api.GetTMDBUpcomingMovies) + movie.GET("/:id", api.GetTMDBMovie) + movie.GET("/:id/external_ids", api.GetTMDBMovieExternalIDs) + } + + // Search endpoints + search := tmdb.Group("/search") + { + search.GET("/movie", api.SearchTMDBMovies) + search.GET("/tv", api.SearchTMDBTV) + } + + // TV endpoints + tv := tmdb.Group("/tv") + { + tv.GET("/:id/external_ids", api.GetTMDBTVExternalIDs) + } + + // Discover endpoints + discover := tmdb.Group("/discover") + { + discover.GET("/movie", api.DiscoverMovies) + discover.GET("/tv", api.DiscoverTV) + } + } + } + + // Admin API + admin := r.Group("/admin") + { + // Movies endpoints + adminMovies := admin.Group("/movies") + { + adminMovies.GET("", api.GetAdminMovies) + adminMovies.POST("/toggle-visibility", api.ToggleMovieVisibility) + } + + // Users endpoints + adminUsers := admin.Group("/users") + { + adminUsers.GET("", api.GetUsers) + adminUsers.POST("/create", api.CreateUser) + adminUsers.POST("/toggle-admin", api.ToggleAdmin) + adminUsers.POST("/send-verification", api.SendVerification) + adminUsers.POST("/verify-code", api.VerifyCode) + } + } + + // Запускаем сервер + port := os.Getenv("PORT") + if err := r.Run(":" + port); err != nil { + log.Fatal(err) + } +} diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..e900a92 --- /dev/null +++ b/render.yaml @@ -0,0 +1,13 @@ +services: + - type: web + name: neomovies-api + env: go + buildCommand: go build -o app + startCommand: ./app + envVars: + - key: GIN_MODE + value: release + - key: TMDB_ACCESS_TOKEN + sync: false + healthCheckPath: /health + autoDeploy: true diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..a2046a0 --- /dev/null +++ b/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Переходим в директорию с приложением +cd "$HOME/neomovies-api" + +# Запускаем приложение +PORT=$PORT GIN_MODE=release ./app