Unit3DwebUp 0.0.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. config/__init__.py +4 -0
  2. config/api_data.py +28 -0
  3. config/constants.py +34 -0
  4. config/host_data.py +47 -0
  5. config/itt.py +154 -0
  6. config/logger.py +37 -0
  7. config/settings.py +212 -0
  8. config/sis.py +137 -0
  9. config/tags.py +395 -0
  10. config/trackers.py +47 -0
  11. external/__init__.py +0 -0
  12. external/async_http_client_service.py +48 -0
  13. external/websocket.py +41 -0
  14. models/__init__.py +0 -0
  15. models/interfaces.py +39 -0
  16. models/keywords.py +13 -0
  17. models/media.py +597 -0
  18. models/media_info.py +142 -0
  19. models/movie.py +287 -0
  20. models/tv.py +266 -0
  21. models/tvdb_search.py +102 -0
  22. models/videos.py +26 -0
  23. repositories/__init__.py +0 -0
  24. repositories/db_online.py +82 -0
  25. repositories/interfaces.py +59 -0
  26. repositories/job_repos.py +166 -0
  27. repositories/media_info_factory.py +28 -0
  28. services/__init__.py +0 -0
  29. services/auto_async_service.py +237 -0
  30. services/create_torrent_service.py +94 -0
  31. services/interfaces.py +72 -0
  32. services/itt_tracker_helper.py +463 -0
  33. services/itt_tracker_service.py +85 -0
  34. services/lifespan_service.py +58 -0
  35. services/media_service.py +114 -0
  36. services/tags_service.py +389 -0
  37. services/tmdb.py +246 -0
  38. services/torrent_client_service.py +92 -0
  39. services/torrent_service.py +107 -0
  40. services/tvdb.py +65 -0
  41. services/utility.py +433 -0
  42. services/video_service.py +356 -0
  43. unit3dwebup-0.0.14.dist-info/METADATA +191 -0
  44. unit3dwebup-0.0.14.dist-info/RECORD +53 -0
  45. unit3dwebup-0.0.14.dist-info/WHEEL +5 -0
  46. unit3dwebup-0.0.14.dist-info/entry_points.txt +2 -0
  47. unit3dwebup-0.0.14.dist-info/top_level.txt +6 -0
  48. use_case/__init__.py +0 -0
  49. use_case/make_torrent_usecase.py +67 -0
  50. use_case/process_all_usecase.py +43 -0
  51. use_case/scan_media_usecase.py +133 -0
  52. use_case/seed_usecase.py +117 -0
  53. use_case/upload_usecase.py +77 -0
models/tvdb_search.py ADDED
@@ -0,0 +1,102 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from models.interfaces import MediaRepoInterface
6
+
7
+
8
+ # slots, non avendo la necessità di aggiungere nuovi attributi al volo
9
+ @dataclass(slots=True)
10
+ class TvdbRemoteID:
11
+ id: str
12
+ sourceName: str
13
+ type: int
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class TvdbSearchResult(MediaRepoInterface):
18
+ """
19
+ Dataclass for manage result from a TVDB search
20
+ TMDB e TVDB share the same interface
21
+ """
22
+ id: str
23
+ tvdb_id: str
24
+ name: str
25
+ extended_title: str | None
26
+ slug: str | None
27
+ type: str
28
+ primary_type: str
29
+ status: str | None
30
+ year: str | None
31
+ first_air_time: str | None
32
+ country: str | None
33
+ primary_language: str | None
34
+ director: str | None # for movies
35
+
36
+ studios: list[str] = field(default_factory=list)
37
+ genres: list[str] = field(default_factory=list)
38
+
39
+ overview: str | None = None
40
+ overviews: dict[str, str] = field(default_factory=dict)
41
+ translations: dict[str, str] = field(default_factory=dict)
42
+
43
+ image_url: str | None = None
44
+ thumbnail: str | None = None
45
+
46
+ # for imdb id
47
+ remote_ids: list[TvdbRemoteID] = field(default_factory=list)
48
+
49
+ @classmethod
50
+ def from_dict(cls, item: dict) -> TvdbSearchResult:
51
+ remote_ids = [TvdbRemoteID(**r) for r in item.get("remote_ids", [])]
52
+
53
+ return cls(
54
+ id=item.get("id", ""),
55
+ tvdb_id=item.get("tvdb_id", "0"),
56
+ name=item.get("name", ""),
57
+ extended_title=item.get("extended_title"),
58
+ slug=item.get("slug"),
59
+ type=item.get("type", ""),
60
+ primary_type=item.get("primary_type", ""),
61
+ status=item.get("status"),
62
+ year=item.get("year"),
63
+ first_air_time=item.get("first_air_time"),
64
+ country=item.get("country"),
65
+ primary_language=item.get("primary_language"),
66
+ director=item.get("director"),
67
+ studios=item.get("studios", []),
68
+ genres=item.get("genres", []),
69
+ overview=item.get("overview"),
70
+ overviews=item.get("overviews", {}),
71
+ translations=item.get("translations", {}),
72
+ image_url=item.get("image_url"),
73
+ thumbnail=item.get("thumbnail"),
74
+ remote_ids=remote_ids,
75
+ )
76
+
77
+ def get_title(self) -> str:
78
+ return self.name
79
+
80
+ def get_original(self) -> str:
81
+ return self.extended_title or self.name
82
+
83
+ def get_date(self) -> str:
84
+ return self.first_air_time or ""
85
+
86
+ def get_id(self) -> int:
87
+ try:
88
+ return int(self.tvdb_id)
89
+ except ValueError:
90
+ return 0
91
+
92
+ def get_poster_path(self) -> str:
93
+ return self.image_url or ""
94
+
95
+ def get_imdb(self) -> str | None:
96
+ for r in self.remote_ids:
97
+ if 'IMDB' in r.sourceName.upper():
98
+ return r.id.lower().replace('tt', '')
99
+ return None
100
+
101
+ def get_translations(self) -> dict[str, str] | None:
102
+ return self.translations
models/videos.py ADDED
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class Videos:
8
+ """
9
+ Dataclass for manage result from a TMDB search : Trailers
10
+ """
11
+ id: str
12
+ iso_3166_1: str
13
+ iso_639_1: str
14
+ key: str
15
+ name: str
16
+ official: bool
17
+ published_at: str
18
+ site: str
19
+ size: int
20
+ type: str
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class Data:
25
+ id: int
26
+ results: list[Videos]
File without changes
@@ -0,0 +1,82 @@
1
+ # -*- coding: utf-8 -*-
2
+ from typing import TypeVar
3
+ import aiohttp
4
+
5
+ from models.interfaces import MediaRepoInterface
6
+ from models.movie import AltTitle, NowPlaying
7
+ from models.tv import OnTheAir, DataResponse
8
+ from models.keywords import Keyword
9
+ from models.videos import Data
10
+
11
+ from repositories.interfaces import MovieRepositoryInterface
12
+
13
+ from services.tmdb import TmdbAsyncAPI
14
+ from services.tvdb import TvdbAsyncAPI
15
+
16
+ T = TypeVar('T')
17
+
18
+
19
+ class Tmdb(TmdbAsyncAPI, MovieRepositoryInterface):
20
+ """
21
+ the purpose is providing a common interface for calling service classes
22
+ while keeping the service implementations separate
23
+ It is an update of the old code unit3dup 0.8.21
24
+ """
25
+
26
+ def __init__(self, session: aiohttp.ClientSession | None = None):
27
+ super().__init__(session)
28
+
29
+ async def search(self, query: str, category: str) -> list[T] | None:
30
+ result = await super().search(query=query, category=category)
31
+ return result
32
+
33
+ async def alternative(self, movie_id: int, category: str) -> AltTitle | DataResponse | None:
34
+ return await super().alternative(movie_id=movie_id, category=category)
35
+
36
+ async def videos(self, movie_id: int, category: str) -> Data | None:
37
+ return await super().videos(movie_id=movie_id, category=category)
38
+
39
+ async def details(self, video_id: int, category: str) -> Data | None:
40
+ return await super().details(video_id=video_id, category=category)
41
+
42
+ async def keywords(self, movie_id: int, category: str) -> Keyword | None:
43
+ return await super().keywords(movie_id=movie_id, category=category)
44
+
45
+ async def playing(self, category: str) -> list[NowPlaying] | list[OnTheAir] | None:
46
+ return await super().playing(category=category)
47
+
48
+ async def close(self):
49
+ await self.session.close()
50
+
51
+
52
+ class Tvdb(TvdbAsyncAPI, MovieRepositoryInterface):
53
+ """
54
+ the purpose is providing a common interface for calling service classes
55
+ while keeping the service implementations separate
56
+ It is an update of the old code unit3dup 0.8.21
57
+ """
58
+
59
+ def __init__(self, session: aiohttp.ClientSession | None = None):
60
+ super().__init__(session)
61
+
62
+ async def search(self, query: str, category: str = None) -> list[MediaRepoInterface]:
63
+ await super().tvdb_login()
64
+ return await super().search(query=query, category=category)
65
+
66
+ async def alternative(self, movie_id: int, category: str) -> AltTitle | DataResponse | None:
67
+ pass
68
+
69
+ async def videos(self, movie_id: int, category: str) -> Data | None:
70
+ pass
71
+
72
+ async def details(self, video_id: int, category: str) -> Data | None:
73
+ pass
74
+
75
+ async def keywords(self, movie_id: int, category: str) -> Keyword | None:
76
+ pass
77
+
78
+ async def playing(self, category: str) -> list[NowPlaying] | list[OnTheAir] | None:
79
+ pass
80
+
81
+ async def close(self):
82
+ await self.session.close()
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+ from abc import ABC, abstractmethod
3
+
4
+ from models.interfaces import MediaRepoInterface
5
+ from models.movie import Movie, NowPlaying
6
+ from models.keywords import Keyword
7
+ from models.videos import Data
8
+
9
+ from fastapi import FastAPI
10
+
11
+
12
+ class MovieRepositoryInterface(ABC):
13
+ """
14
+ an Interface class for TMDB and TVDB to share the same methods
15
+ """
16
+
17
+ @abstractmethod
18
+ async def search(self, query: str, category: str = None) -> list[MediaRepoInterface]: ...
19
+
20
+ @abstractmethod
21
+ async def alternative(self, movie_id: int, category: str) -> list: ...
22
+
23
+ @abstractmethod
24
+ async def videos(self, movie_id: int, category: str) -> Data: ...
25
+
26
+ @abstractmethod
27
+ async def details(self, movie_id: int, category: str) -> Movie: ...
28
+
29
+ @abstractmethod
30
+ async def keywords(self, movie_id: int, category: str) -> list[Keyword]: ...
31
+
32
+ @abstractmethod
33
+ async def playing(self, category: str) -> list[NowPlaying]: ...
34
+
35
+ @abstractmethod
36
+ async def close(self) -> None: ...
37
+
38
+
39
+ class JobRepositoryInterface(ABC):
40
+ """
41
+ an Interface class for Redis to share the same methods with other repositories that are not yet implemented
42
+ """
43
+ @abstractmethod
44
+ async def connect(self, app: FastAPI): ...
45
+
46
+ @abstractmethod
47
+ async def create_job(self, job_id: str, data: dict): ...
48
+
49
+ @abstractmethod
50
+ async def create_profile(self, data: dict): ...
51
+
52
+ @abstractmethod
53
+ async def update_job(self, job_id: str, new_job: dict): ...
54
+
55
+ @abstractmethod
56
+ async def delete_job_list(self, job_id: str): ...
57
+
58
+ @abstractmethod
59
+ async def close(self): ...
@@ -0,0 +1,166 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import json
4
+ import os
5
+ import signal
6
+ from config.logger import get_logger
7
+ from fastapi import FastAPI
8
+
9
+ from repositories.interfaces import JobRepositoryInterface
10
+ import redis.asyncio as redis
11
+ from redis import exceptions as redis_exceptions, Redis
12
+
13
+
14
+ class JobRedisRepo(JobRepositoryInterface):
15
+ """
16
+ Manage Redis connections
17
+
18
+ Save Media data to Redis
19
+
20
+ Redis instance is stored in FastAPi app.state
21
+
22
+ The purpose is to avoid waste calls to scanner and share data across endpoints and other..(later)
23
+
24
+ """
25
+
26
+ def __init__(self, url: str):
27
+ """
28
+ :param url: Redis server url
29
+ """
30
+ self.redis = redis.from_url(url, decode_responses=True)
31
+ self.logger = get_logger(self.__class__.__name__)
32
+
33
+ async def connect(self, app: FastAPI) -> Redis | None:
34
+ """
35
+ :param app: FastAPi app instance
36
+ :return: the redis instance stored in the fastapi app
37
+ """
38
+ app.state.redis = self.redis
39
+
40
+ try:
41
+ if await self.redis.ping(): print("INfO: DB connected to redis://localhost:6379")
42
+ return self.redis
43
+ except redis_exceptions.ConnectionError as e:
44
+ self.logger.error("Database connection error")
45
+ os.kill(os.getpid(), signal.SIGTERM)
46
+ return None
47
+
48
+ async def create_job(self, job_id: str, data: dict):
49
+ """
50
+ :param job_id: Job id is the file or folder path hashed stored in the Media class object
51
+ :param data: a dict che store the data of Media object ( media to dict())
52
+ :return:
53
+
54
+ Create a new job
55
+ Each job represents a Media file or folder
56
+
57
+ Every Media object contains a job_id (hash)
58
+ """
59
+ try:
60
+ await self.redis.hset(job_id, mapping={
61
+ "data": json.dumps(data),
62
+ })
63
+ except Exception as e:
64
+ self.logger.error(e)
65
+ return job_id
66
+
67
+ async def create_job_list(self, job_id: str, job_list: list):
68
+ """
69
+ :param job_id: Job id is the scan path hashed
70
+ :param job_list: a list of job id
71
+ :return:
72
+
73
+ It represents a page on the frontend that is a list of Poster
74
+
75
+ We use joblist to manage group of posters
76
+
77
+ Each poster is a job id
78
+ """
79
+ await self._save_list(job_id, job_list)
80
+
81
+ async def get_job_list(self, job_id: str) -> list:
82
+ """
83
+ :param job_id: the job id list
84
+ :return: a list of job id
85
+
86
+ Get a list of Jobs ID
87
+
88
+ it represents a page on the frontend
89
+
90
+ For example we load the page on start to avoid running a new scan on the ssd and requests to TMDB,TVDB
91
+ """
92
+ ids = await self.redis.smembers(f"{job_id}:ids")
93
+ return list(set(ids))
94
+
95
+ async def create_profile(self, data: dict):
96
+ """
97
+ :param data: a dict che store the configuration file read from the server
98
+ :return:
99
+
100
+ Create a new job that includes the user preferences read from the configuration file
101
+
102
+ You can see the preferences on the preferences page
103
+ """
104
+ await self.create_job(job_id='0', data=data)
105
+
106
+ async def get_job(self, job_id: str):
107
+ """
108
+ :param job_id: Job id is the file or folder path hashed stored in the Media class object
109
+ :return: a dict that stores data about Media object
110
+ """
111
+ return await self.redis.hget(job_id, 'data')
112
+
113
+ async def update_job(self, job_id: str, new_data: dict):
114
+ """
115
+ :param job_id: Job id is the file or folder path hashed stored in the Media class object
116
+ :param new_data: a dict that stores new data
117
+ :return:
118
+
119
+ Update a specific field of a job
120
+ """
121
+
122
+ current = await self.redis.hget(job_id, "data")
123
+ if current:
124
+ data = json.loads(current)
125
+ else:
126
+ data = {}
127
+
128
+ data.update(new_data)
129
+
130
+ await self.redis.hset(job_id, mapping={
131
+ "data": json.dumps(data),
132
+ })
133
+
134
+ return job_id
135
+
136
+ async def _save_list(self, job_id: str, job_list_id: list):
137
+ """
138
+ :param job_id: the job id list
139
+ :param job_list_id: a list of job id
140
+ :return:
141
+
142
+ Replace an old job list with a new one
143
+
144
+ for example if the user moves or deletes or adds new file in the scan folder
145
+
146
+ """
147
+ await self.redis.delete(f"{job_id}:ids")
148
+ await self.redis.sadd(f"{job_id}:ids", *job_list_id)
149
+
150
+ async def delete_job_list(self, job_id: str):
151
+ """
152
+ :param job_id: the job id list
153
+ :return:
154
+
155
+ Delete an old job list
156
+
157
+ For example if the user wipes the scan folder
158
+ """
159
+
160
+ await self.redis.delete(f"{job_id}:ids")
161
+
162
+ async def close(self):
163
+ """
164
+ Close connection to Redis server
165
+ """
166
+ await self.redis.close()
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import asyncio
4
+
5
+ from pymediainfo import MediaInfo
6
+ from models.media_info import MediaFile
7
+
8
+
9
+ class MediaFileFactory:
10
+
11
+ @staticmethod
12
+ async def from_path(path: str) -> MediaFile:
13
+ """
14
+ Try to run pymediainfo without blocking
15
+ """
16
+ info = await asyncio.to_thread(MediaInfo.parse, path)
17
+ data = info.to_data().get("tracks", [])
18
+
19
+ video = [track for track in data if track.get("track_type") == "Video"]
20
+ audio = [track for track in data if track.get("track_type") == "Audio"]
21
+ general = next((track for track in data if track.get("track_type") == "General"), {})
22
+
23
+ return MediaFile(
24
+ file_path=path,
25
+ video_tracks=video,
26
+ audio_tracks=audio,
27
+ general_track=general,
28
+ )
services/__init__.py ADDED
File without changes