remove autosync files (#1241)

This commit is contained in:
acelinkio 2025-12-23 12:42:31 -08:00 committed by GitHub
parent 651d721669
commit 2db9204064
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 0 additions and 381 deletions

View File

@ -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
View File

@ -1 +0,0 @@
__pycache__

View File

@ -1,8 +0,0 @@
FROM python:3.13
WORKDIR /app
COPY ./requirements.txt .
RUN pip3 install -r ./requirements.txt
COPY . .
ENTRYPOINT ["python3", "-m", "autosync"]

View File

@ -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)

View File

@ -1,6 +0,0 @@
#!/usr/bin/env python
import asyncio
import autosync
asyncio.run(autosync.main())

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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]

View File

@ -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
)

View File

@ -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

View File

@ -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
}

View File

@ -1,2 +0,0 @@
[tool.ruff.format]
indent-style = "tab"

View File

@ -1,3 +0,0 @@
aio-pika
msgspec
requests