Skip to content

Commit

Permalink
Add simkl automatic syncing (#347)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoriya authored Mar 23, 2024
2 parents 6143125 + 8bbccd4 commit 1cd3704
Show file tree
Hide file tree
Showing 44 changed files with 1,016 additions and 188 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ THEMOVIEDB_APIKEY=
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
PUBLIC_URL=http://localhost:5000

# Use a builtin oidc service (google or discord):
# Use a builtin oidc service (google, discord, or simkl):
# When you create a client_id, secret combo you may be asked for a redirect url. You need to specify https://YOUR-PUBLIC-URL/api/auth/logged/YOUR-SERVICE-NAME
OIDC_DISCORD_CLIENTID=
OIDC_DISCORD_SECRET=
Expand Down Expand Up @@ -73,3 +73,7 @@ POSTGRES_PORT=5432

MEILI_HOST="http://meilisearch:7700"
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"

RABBITMQ_HOST=rabbitmq
RABBITMQ_DEFAULT_USER=kyoo
RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
5 changes: 1 addition & 4 deletions .github/workflows/coding-style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,8 @@ jobs:
run: yarn lint && yarn format

scanner:
name: "Lint scanner"
name: "Lint scanner/autosync"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./scanner
steps:
- uses: actions/checkout@v4

Expand Down
1 change: 1 addition & 0 deletions autosync/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
8 changes: 8 additions & 0 deletions autosync/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:3.12
WORKDIR /app

COPY ./requirements.txt .
RUN pip3 install -r ./requirements.txt

COPY . .
ENTRYPOINT ["python3", "-m", "autosync"]
66 changes: 66 additions & 0 deletions autosync/autosync/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import logging
import os

import dataclasses_json
from datetime import datetime
from marshmallow import fields

dataclasses_json.cfg.global_config.encoders[datetime] = datetime.isoformat
dataclasses_json.cfg.global_config.decoders[datetime] = datetime.fromisoformat
dataclasses_json.cfg.global_config.mm_fields[datetime] = fields.DateTime(format="iso")
dataclasses_json.cfg.global_config.encoders[datetime | None] = datetime.isoformat
dataclasses_json.cfg.global_config.decoders[datetime | None] = datetime.fromisoformat
dataclasses_json.cfg.global_config.mm_fields[datetime | None] = fields.DateTime(
format="iso"
)

import pika
from pika import spec
from pika.adapters.blocking_connection import BlockingChannel
import pika.credentials
from autosync.models.message import Message
from autosync.services.aggregate import Aggregate

from autosync.services.simkl import Simkl


logging.basicConfig(level=logging.INFO)
service = Aggregate([Simkl()])


def on_message(
ch: BlockingChannel,
method: spec.Basic.Deliver,
properties: spec.BasicProperties,
body: bytes,
):
try:
message = Message.from_json(body) # type: Message
service.update(message.value.user, message.value.resource, message.value)
except Exception as e:
logging.exception("Error processing message.", exc_info=e)
logging.exception("Body: %s", body)


def main():
connection = pika.BlockingConnection(
pika.ConnectionParameters(
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
credentials=pika.credentials.PlainCredentials(
os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
),
)
)
channel = connection.channel()

channel.exchange_declare(exchange="events.watched", exchange_type="topic")
result = channel.queue_declare("", exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange="events.watched", queue=queue_name, routing_key="#")

channel.basic_consume(
queue=queue_name, on_message_callback=on_message, auto_ack=True
)
logging.info("Listening for autosync.")
channel.start_consuming()
4 changes: 4 additions & 0 deletions autosync/autosync/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env python
import autosync

autosync.main()
18 changes: 18 additions & 0 deletions autosync/autosync/models/episode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Literal
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase

from autosync.models.show import Show

from .metadataid import MetadataID


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Episode:
external_id: dict[str, MetadataID]
show: Show
season_number: int
episode_number: int
absolute_number: int
kind: Literal["episode"]
23 changes: 23 additions & 0 deletions autosync/autosync/models/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase

from autosync.models.episode import Episode
from autosync.models.movie import Movie
from autosync.models.show import Show
from autosync.models.user import User
from autosync.models.watch_status import WatchStatus


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class WatchStatusMessage(WatchStatus):
user: User
resource: Movie | Show | Episode


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Message:
action: str
type: str
value: WatchStatusMessage
10 changes: 10 additions & 0 deletions autosync/autosync/models/metadataid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase
from typing import Optional


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class MetadataID:
data_id: str
link: Optional[str]
19 changes: 19 additions & 0 deletions autosync/autosync/models/movie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Literal, Optional
from datetime import datetime
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase

from .metadataid import MetadataID


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Movie:
name: str
air_date: Optional[datetime]
external_id: dict[str, MetadataID]
kind: Literal["movie"]

@property
def year(self):
return self.air_date.year if self.air_date is not None else None
19 changes: 19 additions & 0 deletions autosync/autosync/models/show.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Literal, Optional
from datetime import datetime
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase

from .metadataid import MetadataID


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Show:
name: str
start_air: Optional[datetime]
external_id: dict[str, MetadataID]
kind: Literal["show"]

@property
def year(self):
return self.start_air.year if self.start_air is not None else None
34 changes: 34 additions & 0 deletions autosync/autosync/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from datetime import datetime, time
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase
from typing import Optional


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class JwtToken:
token_type: str
access_token: str
refresh_token: Optional[str]
expire_in: time
expire_at: datetime


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class ExternalToken:
id: str
username: str
profileUrl: Optional[str]
token: JwtToken


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class User:
id: str
username: str
email: str
permissions: list[str]
settings: dict[str, str]
external_id: dict[str, ExternalToken]
23 changes: 23 additions & 0 deletions autosync/autosync/models/watch_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from datetime import datetime
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase
from typing import Optional
from enum import Enum


class Status(str, Enum):
COMPLETED = "Completed"
WATCHING = "Watching"
DROPED = "Droped"
PLANNED = "Planned"
DELETED = "Deleted"


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class WatchStatus:
added_date: datetime
played_date: Optional[datetime]
status: Status
watched_time: Optional[int]
watched_percent: Optional[int]
26 changes: 26 additions & 0 deletions autosync/autosync/services/aggregate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import logging
from autosync.services.service import Service
from ..models.user import User
from ..models.show import Show
from ..models.movie import Movie
from ..models.episode import Episode
from ..models.watch_status import WatchStatus


class Aggregate(Service):
def __init__(self, services: list[Service]):
self._services = [x for x in services if x.enabled]
logging.info("Autosync enabled with %s", [x.name for x in self._services])

@property
def name(self) -> str:
return "aggragate"

def update(self, user: User, resource: Movie | Show | Episode, status: WatchStatus):
for service in self._services:
try:
service.update(user, resource, status)
except Exception as e:
logging.exception(
"Unhandled error on autosync %s:", service.name, exc_info=e
)
21 changes: 21 additions & 0 deletions autosync/autosync/services/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from abc import abstractmethod, abstractproperty

from ..models.user import User
from ..models.show import Show
from ..models.movie import Movie
from ..models.episode import Episode
from ..models.watch_status import WatchStatus


class Service:
@abstractproperty
def name(self) -> str:
raise NotImplementedError

@abstractproperty
def enabled(self) -> bool:
return True

@abstractmethod
def update(self, user: User, resource: Movie | Show | Episode, status: WatchStatus):
raise NotImplementedError
Loading

0 comments on commit 1cd3704

Please sign in to comment.