mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-02-04 01:53:44 -05:00
remove autosync files (#1241)
This commit is contained in:
parent
651d721669
commit
2db9204064
@ -1,12 +0,0 @@
|
||||
# vi: ft=sh
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
# RabbitMQ settings
|
||||
# URL examples: https://docs.aio-pika.com/#url-examples
|
||||
# This uses AIORMQ (https://github.com/mosquito/aiormq/) under the hood, and supports whatever the library supports.
|
||||
# RABBITMQ_URL=ampqs://user:password@rabbitmq-server:1234/vhost?capath=/path/to/cacert.pem&certfile=/path/to/cert.pem&keyfile=/path/to/key.pem
|
||||
# These values are ignored when the RABBITMQ_URL is set
|
||||
RABBITMQ_HOST=rabbitmq
|
||||
RABBITMQ_PORT=5672
|
||||
RABBITMQ_USER=guest
|
||||
RABBITMQ_PASSWORD=guest
|
||||
1
autosync/.gitignore
vendored
1
autosync/.gitignore
vendored
@ -1 +0,0 @@
|
||||
__pycache__
|
||||
@ -1,8 +0,0 @@
|
||||
FROM python:3.13
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./requirements.txt .
|
||||
RUN pip3 install -r ./requirements.txt
|
||||
|
||||
COPY . .
|
||||
ENTRYPOINT ["python3", "-m", "autosync"]
|
||||
@ -1,11 +0,0 @@
|
||||
async def main():
|
||||
import logging
|
||||
from autosync.services.simkl import Simkl
|
||||
from autosync.services.aggregate import Aggregate
|
||||
from autosync.consumer import Consumer
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
service = Aggregate([Simkl()])
|
||||
async with Consumer() as consumer:
|
||||
await consumer.listen(service)
|
||||
@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import asyncio
|
||||
import autosync
|
||||
|
||||
asyncio.run(autosync.main())
|
||||
@ -1,52 +0,0 @@
|
||||
import asyncio
|
||||
from msgspec import json
|
||||
import os
|
||||
from logging import getLogger
|
||||
from aio_pika import ExchangeType, connect_robust
|
||||
from aio_pika.abc import AbstractIncomingMessage
|
||||
|
||||
from autosync.services.service import Service
|
||||
from autosync.models.message import Message
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Consumer:
|
||||
QUEUE = "autosync"
|
||||
|
||||
async def __aenter__(self):
|
||||
self._con = await connect_robust(
|
||||
os.environ.get("RABBITMQ_URL"),
|
||||
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
||||
port=int(os.environ.get("RABBITMQ_PORT", "5672")),
|
||||
login=os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
||||
password=os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
||||
)
|
||||
self._channel = await self._con.channel()
|
||||
self._exchange = await self._channel.declare_exchange(
|
||||
"events.watched", type=ExchangeType.TOPIC
|
||||
)
|
||||
self._queue = await self._channel.declare_queue(self.QUEUE)
|
||||
await self._queue.bind(self._exchange, routing_key="#")
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, exc_tb):
|
||||
await self._con.close()
|
||||
|
||||
async def listen(self, service: Service):
|
||||
async def on_message(message: AbstractIncomingMessage):
|
||||
try:
|
||||
msg = json.decode(message.body, type=Message)
|
||||
service.update(msg.value.user, msg.value.resource, msg.value)
|
||||
await message.ack()
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled error", exc_info=e)
|
||||
await message.reject()
|
||||
|
||||
# Allow up to 20 requests to run in parallel on the same listener.
|
||||
# Since most work is calling API not doing that is a waste.
|
||||
await self._channel.set_qos(prefetch_count=20)
|
||||
await self._queue.consume(on_message)
|
||||
logger.info("Listening for autosync.")
|
||||
await asyncio.Future()
|
||||
@ -1,12 +0,0 @@
|
||||
from msgspec import Struct
|
||||
from autosync.models.show import Show
|
||||
|
||||
from .metadataid import EpisodeID
|
||||
|
||||
|
||||
class Episode(Struct, rename="camel", tag_field="kind", tag="episode"):
|
||||
external_id: dict[str, EpisodeID]
|
||||
show: Show
|
||||
season_number: int
|
||||
episode_number: int
|
||||
absolute_number: int
|
||||
@ -1,17 +0,0 @@
|
||||
from msgspec import Struct
|
||||
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
|
||||
|
||||
|
||||
class WatchStatusMessage(WatchStatus):
|
||||
user: User
|
||||
resource: Movie | Show | Episode
|
||||
|
||||
|
||||
class Message(Struct, rename="camel"):
|
||||
action: str
|
||||
type: str
|
||||
value: WatchStatusMessage
|
||||
@ -1,14 +0,0 @@
|
||||
from msgspec import Struct
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class MetadataID(Struct, rename="camel"):
|
||||
data_id: str
|
||||
link: Optional[str]
|
||||
|
||||
|
||||
class EpisodeID(Struct, rename="camel"):
|
||||
show_id: str
|
||||
season: Optional[int]
|
||||
episode: int
|
||||
link: Optional[str]
|
||||
@ -1,15 +0,0 @@
|
||||
from typing import Optional
|
||||
from datetime import date
|
||||
from msgspec import Struct
|
||||
|
||||
from .metadataid import MetadataID
|
||||
|
||||
|
||||
class Movie(Struct, rename="camel", tag_field="kind", tag="movie"):
|
||||
name: str
|
||||
air_date: Optional[date]
|
||||
external_id: dict[str, MetadataID]
|
||||
|
||||
@property
|
||||
def year(self):
|
||||
return self.air_date.year if self.air_date is not None else None
|
||||
@ -1,15 +0,0 @@
|
||||
from typing import Optional
|
||||
from datetime import date
|
||||
from msgspec import Struct
|
||||
|
||||
from .metadataid import MetadataID
|
||||
|
||||
|
||||
class Show(Struct, rename="camel", tag_field="kind", tag="show"):
|
||||
name: str
|
||||
start_air: Optional[date]
|
||||
external_id: dict[str, MetadataID]
|
||||
|
||||
@property
|
||||
def year(self):
|
||||
return self.start_air.year if self.start_air is not None else None
|
||||
@ -1,26 +0,0 @@
|
||||
from msgspec import Struct
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class JwtToken(Struct):
|
||||
token_type: str
|
||||
access_token: str
|
||||
refresh_token: Optional[str]
|
||||
expire_at: datetime
|
||||
|
||||
|
||||
class ExternalToken(Struct, rename="camel"):
|
||||
id: str
|
||||
username: str
|
||||
profile_url: Optional[str]
|
||||
token: JwtToken
|
||||
|
||||
|
||||
class User(Struct, rename="camel", tag_field="kind", tag="user"):
|
||||
id: str
|
||||
username: str
|
||||
email: str
|
||||
permissions: list[str]
|
||||
settings: dict[str, str]
|
||||
external_id: dict[str, ExternalToken]
|
||||
@ -1,21 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
from msgspec import Struct
|
||||
|
||||
|
||||
class Status(str, Enum):
|
||||
COMPLETED = "Completed"
|
||||
WATCHING = "Watching"
|
||||
DROPED = "Droped"
|
||||
PLANNED = "Planned"
|
||||
DELETED = "Deleted"
|
||||
|
||||
|
||||
class WatchStatus(Struct, rename="camel"):
|
||||
added_date: datetime
|
||||
played_date: Optional[datetime]
|
||||
status: Status
|
||||
watched_time: Optional[int]
|
||||
watched_percent: Optional[int]
|
||||
@ -1,28 +0,0 @@
|
||||
from logging import getLogger
|
||||
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
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Aggregate(Service):
|
||||
def __init__(self, services: list[Service]):
|
||||
self._services = [x for x in services if x.enabled]
|
||||
logger.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:
|
||||
logger.exception(
|
||||
"Unhandled error on autosync %s:", service.name, exc_info=e
|
||||
)
|
||||
@ -1,21 +0,0 @@
|
||||
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
|
||||
@ -1,117 +0,0 @@
|
||||
import os
|
||||
import requests
|
||||
from logging import getLogger
|
||||
from autosync.models.metadataid import MetadataID
|
||||
|
||||
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, Status
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Simkl(Service):
|
||||
def __init__(self) -> None:
|
||||
self._api_key = os.environ.get("OIDC_SIMKL_CLIENTID")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "simkl"
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self._api_key is not None
|
||||
|
||||
def update(self, user: User, resource: Movie | Show | Episode, status: WatchStatus):
|
||||
if "simkl" not in user.external_id or self._api_key is None:
|
||||
return
|
||||
|
||||
watch_date = status.played_date or status.added_date
|
||||
|
||||
if isinstance(resource, Episode):
|
||||
if status.status != Status.COMPLETED:
|
||||
return
|
||||
|
||||
resp = requests.post(
|
||||
"https://api.simkl.com/sync/history",
|
||||
json={
|
||||
"shows": [
|
||||
{
|
||||
"watched_at": watch_date.isoformat(),
|
||||
"title": resource.show.name,
|
||||
"year": resource.show.year,
|
||||
"ids": self._map_external_ids(resource.show.external_id),
|
||||
"seasons": [
|
||||
{
|
||||
"number": resource.season_number,
|
||||
"episodes": [{"number": resource.episode_number}],
|
||||
},
|
||||
{
|
||||
"number": 1,
|
||||
"episodes": [{"number": resource.absolute_number}],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {user.external_id['simkl'].token.access_token}",
|
||||
"simkl-api-key": self._api_key,
|
||||
},
|
||||
)
|
||||
logger.info("Simkl response: %s %s", resp.status_code, resp.text)
|
||||
return
|
||||
|
||||
category = "movies" if isinstance(resource, Movie) else "shows"
|
||||
|
||||
simkl_status = self._map_status(status.status)
|
||||
if simkl_status is None:
|
||||
return
|
||||
|
||||
resp = requests.post(
|
||||
"https://api.simkl.com/sync/add-to-list",
|
||||
json={
|
||||
category: [
|
||||
{
|
||||
"to": simkl_status,
|
||||
"watched_at": watch_date.isoformat()
|
||||
if status.status == Status.COMPLETED
|
||||
else None,
|
||||
"title": resource.name,
|
||||
"year": resource.year,
|
||||
"ids": self._map_external_ids(resource.external_id),
|
||||
}
|
||||
]
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {user.external_id['simkl'].token.access_token}",
|
||||
"simkl-api-key": self._api_key,
|
||||
},
|
||||
)
|
||||
logger.info("Simkl response: %s %s", resp.status_code, resp.text)
|
||||
|
||||
def _map_status(self, status: Status):
|
||||
match status:
|
||||
case Status.COMPLETED:
|
||||
return "completed"
|
||||
case Status.WATCHING:
|
||||
return "watching"
|
||||
case Status.COMPLETED:
|
||||
return "completed"
|
||||
case Status.PLANNED:
|
||||
return "plantowatch"
|
||||
case Status.DELETED:
|
||||
# do not delete items on simkl, most of deleted status are for a rewatch.
|
||||
return None
|
||||
case _:
|
||||
return None
|
||||
|
||||
def _map_external_ids(self, ids: dict[str, MetadataID]):
|
||||
return {service: id.data_id for service, id in ids.items()} | {
|
||||
"tmdb": int(ids["themoviedatabase"].data_id)
|
||||
if "themoviedatabase" in ids
|
||||
else None
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
[tool.ruff.format]
|
||||
indent-style = "tab"
|
||||
@ -1,3 +0,0 @@
|
||||
aio-pika
|
||||
msgspec
|
||||
requests
|
||||
Loading…
x
Reference in New Issue
Block a user