From ee8e29fb2ac15e57ab7b09546d71ef4a85ea1be2 Mon Sep 17 00:00:00 2001 From: zeroquinc Date: Thu, 23 May 2024 13:11:03 +0200 Subject: [PATCH] feature: add trakt api --- .github/release-drafter.yml | 31 ++++++++++++++ .github/workflows/labeler.yml | 36 +++++++++++++++++ .github/workflows/release-drafter.yml | 21 ++++++++++ api/trakt/__init__.py | 0 api/trakt/client.py | 25 ++++++++++++ api/trakt/endpoints/__init__.py | 0 api/trakt/endpoints/user.py | 29 ++++++++++++++ api/trakt/exceptions.py | 5 +++ api/trakt/models/rating.py | 58 +++++++++++++++++++++++++++ config/globals.py | 11 +++-- main.py | 8 ++-- bot.py => src/discord/bot.py | 0 src/discord/embed.py | 20 +++++++++ webhook.py => src/webhook/hook.py | 0 test.py | 21 ++++++++++ 15 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/release-drafter.yml create mode 100644 api/trakt/__init__.py create mode 100644 api/trakt/client.py create mode 100644 api/trakt/endpoints/__init__.py create mode 100644 api/trakt/endpoints/user.py create mode 100644 api/trakt/exceptions.py create mode 100644 api/trakt/models/rating.py rename bot.py => src/discord/bot.py (100%) create mode 100644 src/discord/embed.py rename webhook.py => src/webhook/hook.py (100%) create mode 100644 test.py diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..781cb8a --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,31 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + + $CHANGES \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..1299631 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,36 @@ +name: Label PRs based on PR title + +on: + pull_request: + types: [opened, synchronize] + +jobs: + labelPR: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install GitHub CLI + run: | + curl -OL https://github.com/cli/cli/releases/download/v2.4.0/gh_2.4.0_linux_amd64.deb + sudo dpkg -i gh_2.4.0_linux_amd64.deb + + - name: Get PR number + id: pr_number + run: echo "::set-output name=number::${{ github.event.pull_request.number }}" + + - name: Add label + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + if [[ "$PR_TITLE" == fix:* ]]; then + gh pr edit ${{ steps.pr_number.outputs.number }} --add-label "fix" + elif [[ "$PR_TITLE" == feature:* ]]; then + gh pr edit ${{ steps.pr_number.outputs.number }} --add-label "feature" + elif [[ "$PR_TITLE" == chore:* ]]; then + gh pr edit ${{ steps.pr_number.outputs.number }} --add-label "chore" + elif [[ "$PR_TITLE" == enhancement:* ]]; then + gh pr edit ${{ steps.pr_number.outputs.number }} --add-label "enhancement" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..0d89aef --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,21 @@ +name: Release Drafter + +on: + push: + branches: + - main + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Install Release Drafter + uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/api/trakt/__init__.py b/api/trakt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/trakt/client.py b/api/trakt/client.py new file mode 100644 index 0000000..43cefbc --- /dev/null +++ b/api/trakt/client.py @@ -0,0 +1,25 @@ +import requests +from config.globals import TRAKT_API_URL, TRAKT_CLIENT_ID +from .exceptions import TraktAPIException + +class TraktClient: + def __init__(self): + self.client_id = TRAKT_CLIENT_ID + self.session = requests.Session() + self.headers = { + "Content-Type": "application/json", + "trakt-api-version": "2", + "trakt-api-key": self.client_id + } + self.session.headers.update(self.headers) + + def _get(self, endpoint, params=None): + url = f'{TRAKT_API_URL}/{endpoint}' + response = self.session.get(url, params=params) + if response.status_code != 200: + raise TraktAPIException(response.json()) + return response.json() + + def user(self, username): + from .endpoints.user import User + return User(self, username) \ No newline at end of file diff --git a/api/trakt/endpoints/__init__.py b/api/trakt/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/trakt/endpoints/user.py b/api/trakt/endpoints/user.py new file mode 100644 index 0000000..a596acb --- /dev/null +++ b/api/trakt/endpoints/user.py @@ -0,0 +1,29 @@ +from dateutil.parser import parse +from dateutil.tz import tzlocal + +from api.trakt.models.rating import Rating + +class User: + def __init__(self, client, username): + self.client = client + self.username = username + + def convert_to_tz_aware(self, dt): + return dt.replace(tzinfo=tzlocal()) if dt is not None else None + + def get_ratings(self, media_type="all", rating=None, start_time=None, end_time=None): + endpoint = f'users/{self.username}/ratings/{media_type}' + params = {} + if rating: + params['rating'] = rating + + ratings = self.client._get(endpoint, params=params) + start_time = self.convert_to_tz_aware(start_time) + end_time = self.convert_to_tz_aware(end_time) + + def is_within_time_range(rating_time): + return (start_time is None or rating_time >= start_time) and (end_time is None or rating_time <= end_time) + + ratings = [Rating(rating) for rating in ratings if is_within_time_range(parse(rating['rated_at']).astimezone(tzlocal()))] + + return ratings \ No newline at end of file diff --git a/api/trakt/exceptions.py b/api/trakt/exceptions.py new file mode 100644 index 0000000..9b7f87d --- /dev/null +++ b/api/trakt/exceptions.py @@ -0,0 +1,5 @@ +class TraktAPIException(Exception): + def __init__(self, error_data): + self.status_code = error_data.get('status_code') + self.message = error_data.get('message') + super().__init__(self.message) \ No newline at end of file diff --git a/api/trakt/models/rating.py b/api/trakt/models/rating.py new file mode 100644 index 0000000..bedd95a --- /dev/null +++ b/api/trakt/models/rating.py @@ -0,0 +1,58 @@ +from dateutil.parser import parse +from dateutil.tz import tzlocal + +class Rating: + def __init__(self, data): + self.type = data['type'] + self.rated = data['rating'] + self.rated_at = parse(data['rated_at']).astimezone(tzlocal()) # Convert to local timezone + self.show_title = None + self.season_id = None + self.episode_id = None + media = create_media(data) + self.set_media_attributes(media) + + def set_media_attributes(self, media): + self.title = media.title + self.year = media.year + + if isinstance(media, (Episode, Season)): + self.show_title = media.show_title + self.season_id = media.season_id + + if isinstance(media, Episode): + self.episode_id = media.episode_id + + @classmethod + def from_json(cls, data): + return cls(data) + +def create_media(data): + media_classes = {'movie': Movie, 'episode': Episode, 'season': Season, 'show': Show} + media_type = data['type'] + return media_classes.get(media_type, lambda _: None)(data) + +class Movie: + def __init__(self, data): + self.title = data['movie']['title'] + self.year = data['movie']['year'] + +class Episode: + def __init__(self, data): + self.title = data['episode']['title'] + self.show_title = data['show']['title'] + self.season_id = "{:02}".format(data['episode']['season']) # Format as two-digit number + self.episode_id = "{:02}".format(data['episode']['number']) # Format as two-digit number + self.year = data['show']['year'] + +class Season: + def __init__(self, data): + self.title = data['show']['title'] + self.show_title = data['show']['title'] + self.season_id = "{:02}".format(data['season']['number']) # Format as two-digit number + self.year = data['show']['year'] + +class Show: + def __init__(self, data): + self.title = data['show']['title'] + self.year = data['show']['year'] \ No newline at end of file diff --git a/config/globals.py b/config/globals.py index b31af2b..ab2634c 100644 --- a/config/globals.py +++ b/config/globals.py @@ -1,16 +1,15 @@ from dotenv import load_dotenv import os -# DotEnv load_dotenv() -# Discord Globals +# Discord DISCORD_SERVER_ID = os.getenv("DISCORD_SERVER_ID") -TOKEN = os.environ["DISCORD_TOKEN"] +DISCORD_TOKEN = os.environ["DISCORD_TOKEN"] # Trakt -TRAKT_USERNAME = "desileR" -TRAKT_CLIENTID = os.getenv("TRAKT_CLIENTID") +TRAKT_API_URL = "https://api.trakt.tv" +TRAKT_CLIENT_ID = os.getenv("TRAKT_CLIENTID") -# API Keys +# TMDB TMDB_API_KEY = os.getenv("TMDB_API_KEY") \ No newline at end of file diff --git a/main.py b/main.py index 09da447..91075af 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,12 @@ import asyncio -from webhook import HandleWebHook -from bot import DiscordBot +from src.webhook.hook import HandleWebHook +from src.discord.bot import DiscordBot -from config.globals import TOKEN +from config.globals import DISCORD_TOKEN if __name__ == "__main__": - discord_bot = DiscordBot(TOKEN) + discord_bot = DiscordBot(DISCORD_TOKEN) webhook = HandleWebHook(discord_bot) loop = asyncio.get_event_loop() diff --git a/bot.py b/src/discord/bot.py similarity index 100% rename from bot.py rename to src/discord/bot.py diff --git a/src/discord/embed.py b/src/discord/embed.py new file mode 100644 index 0000000..5d17b56 --- /dev/null +++ b/src/discord/embed.py @@ -0,0 +1,20 @@ +import discord + +class EmbedBuilder: + def __init__(self, title='', description='', color=discord.Color.default()): + self.embed = discord.Embed(title=title, description=description, color=color) + + def set_author(self, name, icon_url=None, url=None): + self.embed.set_author(name=name, icon_url=icon_url, url=url) + return self + + def add_field(self, name, value, inline=True): + self.embed.add_field(name=name, value=value, inline=inline) + return self + + def set_footer(self, text, icon_url=None): + self.embed.set_footer(text=text, icon_url=icon_url) + return self + + def build(self): + return self.embed \ No newline at end of file diff --git a/webhook.py b/src/webhook/hook.py similarity index 100% rename from webhook.py rename to src/webhook/hook.py diff --git a/test.py b/test.py new file mode 100644 index 0000000..b8f7bc5 --- /dev/null +++ b/test.py @@ -0,0 +1,21 @@ +from datetime import datetime, timedelta +from api.trakt.client import TraktClient + +client = TraktClient() +user = client.user('desiler') + +# Get the current time and the time one hour ago +now = datetime.now() +one_hour_ago = now - timedelta(hours=1) + +#ratings = user.get_ratings(start_time=one_hour_ago, end_time=now) +ratings = user.get_ratings() +for rating in ratings: + print(f"Title: {rating.title}") + print(f"Media type: {rating.type}") + print(f"Season: {rating.season_id}") + print(f"Episode: {rating.episode_id}") + print(f"Rating: {rating.rated}") + print(f"Rated at: {rating.rated_at}") + print(f"Show title: {rating.show_title}") + print() \ No newline at end of file