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/media_info.py ADDED
@@ -0,0 +1,142 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+
4
+ # Based on the old code unit3dup 0.8.21
5
+ import re, os
6
+ from dataclasses import dataclass, field
7
+ from typing import Optional, List, Dict, Any
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class MediaFile:
12
+ """
13
+ Try to create a dataclass for the MediaInfo result
14
+ Many of these data need to be sent to the tracker
15
+ """
16
+ file_path: str
17
+ media_to_string: str | None = None
18
+ video_tracks: List[Dict] = field(default_factory=list)
19
+ audio_tracks: List[Dict] = field(default_factory=list)
20
+ general_track: Dict = field(default_factory=dict)
21
+
22
+ @property
23
+ def media_description(self) -> str:
24
+ return self.media_to_string
25
+
26
+ # --------- VIDEO ---------
27
+ @property
28
+ def codec_id(self) -> str:
29
+ return self.video_tracks[0].get("codec_id", "Unknown") if self.video_tracks else "Unknown"
30
+
31
+ @property
32
+ def video_width(self) -> str:
33
+ return self.video_tracks[0].get("width", "Unknown") if self.video_tracks else "Unknown"
34
+
35
+ @property
36
+ def video_height(self) -> Optional[str]:
37
+ return self.video_tracks[0].get("height") if self.video_tracks else None
38
+
39
+ @property
40
+ def video_scan_type(self) -> Optional[str]:
41
+ return self.video_tracks[0].get("scan_type") if self.video_tracks else None
42
+
43
+ @property
44
+ def video_aspect_ratio(self) -> str:
45
+ return self.video_tracks[0].get("display_aspect_ratio", "Unknown") if self.video_tracks else "Unknown"
46
+
47
+ @property
48
+ def video_frame_rate(self) -> str:
49
+ return self.video_tracks[0].get("frame_rate", "Unknown") if self.video_tracks else "Unknown"
50
+
51
+ @property
52
+ def video_bit_depth(self) -> str:
53
+ return self.video_tracks[0].get("bit_depth", "Unknown") if self.video_tracks else "Unknown"
54
+
55
+ @property
56
+ def video_format(self) -> str:
57
+ return self.video_tracks[0].get("format", "Unknown") if self.video_tracks else "Unknown"
58
+
59
+ # --------- AUDIO ---------
60
+ @property
61
+ def audio_codec_id(self) -> str:
62
+ return self.audio_tracks[0].get("codec_id", "Unknown") if self.audio_tracks else "Unknown"
63
+
64
+ @property
65
+ def audio_bit_rate(self) -> str:
66
+ return self.audio_tracks[0].get("bit_rate", "Unknown") if self.audio_tracks else "Unknown"
67
+
68
+ @property
69
+ def audio_channels(self) -> str:
70
+ return self.audio_tracks[0].get("channels", "Unknown") if self.audio_tracks else "Unknown"
71
+
72
+ @property
73
+ def audio_sampling_rate(self) -> str:
74
+ return self.audio_tracks[0].get("sampling_rate", "Unknown") if self.audio_tracks else "Unknown"
75
+
76
+ @property
77
+ def audio_format(self) -> str:
78
+ return self.audio_tracks[0].get("format", "Unknown") if self.audio_tracks else "Unknown"
79
+
80
+ @property
81
+ def audio_language(self) -> str:
82
+ return self.audio_tracks[0].get("language", "Unknown") if self.audio_tracks else "Unknown"
83
+
84
+ @property
85
+ def subtitle_tracks(self) -> List[Dict]:
86
+ return [t for t in self.video_tracks if t.get("track_type") == "Text"]
87
+
88
+ @property
89
+ def available_languages(self) -> list[Any | None] | list[str]:
90
+ langs = {
91
+ t.get("language")
92
+ for t in (self.audio_tracks + self.subtitle_tracks)
93
+ if t.get("language")
94
+ }
95
+ return list(langs) or ["not found"]
96
+
97
+ @property
98
+ def file_size(self) -> str:
99
+ return self.general_track.get("file_size", "Unknown")
100
+
101
+ @property
102
+ def is_interlaced(self) -> Optional[int]:
103
+ encoding = self.video_tracks[0].get("encoding_settings") if self.video_tracks else None
104
+ if not encoding:
105
+ return None
106
+ match = re.search(r"interlaced=(\d)", encoding)
107
+ return int(match.group(1)) if match else None
108
+
109
+ def generate_release_name(self, guess_title: str, resolution: str) -> Optional[str]:
110
+ if not self.video_tracks:
111
+ return None
112
+
113
+ video_format = self.video_tracks[0].get("format", "")
114
+ audio_format = self.audio_tracks[0].get("format", "")
115
+ _, ext = os.path.splitext(self.file_path)
116
+
117
+ return f"{guess_title}.web-dl.{video_format}.{resolution}.{audio_format}{ext}".lower()
118
+
119
+ def to_dict(self) -> Dict:
120
+ # Converto PosixPath to str
121
+ return {
122
+ "file_path": str(self.file_path),
123
+ "media_to_string": self.media_to_string,
124
+ "video_tracks": self.video_tracks,
125
+ "audio_tracks": self.audio_tracks,
126
+ "general_track": self.general_track,
127
+ }
128
+
129
+ @classmethod
130
+ # non son più necessarie le virgolette per le classi non ancora completamente costruite ad esempio in "MediaFile"
131
+ # basta importare annotations
132
+ def dict_to_mediafile(cls, data: Dict) -> MediaFile:
133
+ """
134
+ From dict to Mediafile
135
+ """
136
+ return cls(
137
+ file_path=data.get("file_path", ""),
138
+ media_to_string=data.get("media_to_string"),
139
+ video_tracks=data.get("video_tracks", []),
140
+ audio_tracks=data.get("audio_tracks", []),
141
+ general_track=data.get("general_track", {}),
142
+ )
models/movie.py ADDED
@@ -0,0 +1,287 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+
4
+ # Same as the old code unit3dup 0.8.21. Add annotations
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from config.logger import get_logger
10
+ from models.interfaces import MediaRepoInterface
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class Title:
15
+ """
16
+ Dataclass for manage result from a TMDB search
17
+ """
18
+ iso_3166_1: str
19
+ title: str
20
+ type: str | None = None
21
+
22
+ @staticmethod
23
+ def from_data(data: dict[str, any]) -> Title | None:
24
+ try:
25
+ return Title(
26
+ iso_3166_1=data["iso_3166_1"],
27
+ title=data["title"],
28
+ type=data.get("type"),
29
+ )
30
+ except KeyError as e:
31
+ print(f"Missing key in Title data: {e}")
32
+ return None
33
+ except TypeError as e:
34
+ print(f"Type error in Title data: {e}")
35
+ return None
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class AltTitle:
40
+ id: int
41
+ titles: list[Title]
42
+
43
+ @classmethod
44
+ def validate(cls, json_str: str) -> AltTitle:
45
+ data = json.loads(json_str)
46
+ id_ = data.get("id", 0)
47
+ titles_data = data.get("titles", [])
48
+ titles = [
49
+ Title.from_data(title_data)
50
+ for title_data in titles_data
51
+ if Title.from_data(title_data) is not None
52
+ ]
53
+ return cls(id=id_, titles=titles)
54
+
55
+
56
+ @dataclass(slots=True)
57
+ class Genre:
58
+ id: int
59
+ name: str
60
+
61
+
62
+ @dataclass(slots=True)
63
+ class ProductionCompany:
64
+ id: int
65
+ logo_path: str | None
66
+ name: str
67
+ origin_country: str
68
+
69
+
70
+ @dataclass(slots=True)
71
+ class ProductionCountry:
72
+ iso_3166_1: str
73
+ name: str
74
+
75
+
76
+ @dataclass(slots=True)
77
+ class SpokenLanguage:
78
+ english_name: str
79
+ iso_639_1: str
80
+ name: str
81
+
82
+
83
+ @dataclass(slots=True)
84
+ class MovieDetails(MediaRepoInterface):
85
+ adult: bool
86
+ backdrop_path: str | None
87
+ belongs_to_collection: str | None
88
+ budget: int
89
+ genres: list[Genre]
90
+ homepage: str
91
+ id: int
92
+ imdb_id: str
93
+ origin_country: list[str]
94
+ original_language: str
95
+ original_title: str
96
+ overview: str
97
+ popularity: float
98
+ poster_path: str | None
99
+ production_companies: list[ProductionCompany]
100
+ production_countries: list[ProductionCountry]
101
+ release_date: str
102
+ revenue: int
103
+ runtime: int
104
+ spoken_languages: list[SpokenLanguage]
105
+ status: str
106
+ tagline: str
107
+ title: str
108
+ video: bool
109
+ vote_average: float
110
+ vote_count: int
111
+
112
+ def get_title(self) -> str:
113
+ return self.title
114
+
115
+ def get_original(self) -> str:
116
+ return self.original_title
117
+
118
+ def get_date(self) -> str:
119
+ return self.release_date
120
+
121
+ def get_id(self) -> int:
122
+ return self.id
123
+
124
+ def get_translations(self) -> dict[str, str] | None:
125
+ pass
126
+
127
+ def get_imdb(self) -> str | None:
128
+ pass
129
+
130
+ def get_poster_path(self) -> str:
131
+ pass
132
+
133
+
134
+ @dataclass(slots=True)
135
+ class NowPlaying:
136
+ """
137
+ Represents Nowplaying attributes
138
+ """
139
+ adult: bool | None = None
140
+ backdrop_path: str | None = None
141
+ genre_ids: list[int] = field(default_factory=list)
142
+ id: int | None = None
143
+ original_language: str | None = None
144
+ original_title: str | None = None
145
+ overview: str | None = None
146
+ popularity: float | None = None
147
+ poster_path: str | None = None
148
+ release_date: str | None = None
149
+ title: str | None = None
150
+ video: bool | None = None
151
+ vote_average: float | None = None
152
+ vote_count: int | None = None
153
+
154
+ def __repr__(self):
155
+ """Returns a string """
156
+ return f"<Movie title={self.title} id={self.id}>"
157
+
158
+
159
+ @dataclass(slots=True)
160
+ class NowPlayingByCountry(NowPlaying):
161
+ """
162
+ Represents a combined movie object NowPlayIng by Country code
163
+ """
164
+ iso_3166_1: str | None = None
165
+ release_dates: list[dict[str, str]] = field(default_factory=list)
166
+
167
+ def __post_init__(self):
168
+ """Validate data """
169
+ if self.iso_3166_1 and (len(self.iso_3166_1) != 2 or not self.iso_3166_1.isalpha()):
170
+ self.iso_3166_1 = None
171
+
172
+ @staticmethod
173
+ def from_data(now_playing: NowPlaying, release_info: MovieReleaseInfo) -> NowPlayingByCountry:
174
+ """
175
+ Creates a NowPlayingByCountry instance from NowPlaying and MovieReleaseInfo instances
176
+ """
177
+ return NowPlayingByCountry(
178
+ adult=now_playing.adult,
179
+ backdrop_path=now_playing.backdrop_path,
180
+ genre_ids=now_playing.genre_ids,
181
+ id=now_playing.id,
182
+ original_language=now_playing.original_language,
183
+ original_title=now_playing.original_title,
184
+ overview=now_playing.overview,
185
+ popularity=now_playing.popularity,
186
+ poster_path=now_playing.poster_path,
187
+ release_date=now_playing.release_date,
188
+ title=now_playing.title,
189
+ video=now_playing.video,
190
+ vote_average=now_playing.vote_average,
191
+ vote_count=now_playing.vote_count,
192
+ iso_3166_1=release_info.iso_3166_1,
193
+ release_dates=release_info.release_dates,
194
+ )
195
+
196
+
197
+ @dataclass(slots=True)
198
+ class Movie(MediaRepoInterface):
199
+ """
200
+ A movie object for the search endpoint
201
+ """
202
+ adult: bool = False
203
+ backdrop_path: str = ''
204
+ genre_ids: list[int] = field(default_factory=list)
205
+ id: int = 0
206
+ original_language: str = ''
207
+ original_title: str = ''
208
+ overview: str = ''
209
+ popularity: float = 0.0
210
+ poster_path: str = ''
211
+ release_date: str = ''
212
+ title: str = ''
213
+ video: bool = False
214
+ vote_average: float = 0.0
215
+ vote_count: int = 0
216
+ softcore: str | None = None
217
+
218
+ def get_title(self) -> str:
219
+ return self.title
220
+
221
+ def get_original(self) -> str:
222
+ return self.original_title
223
+
224
+ def get_date(self) -> str:
225
+ return self.release_date
226
+
227
+ def get_id(self) -> int:
228
+ return self.id
229
+
230
+ def get_poster_path(self) -> str:
231
+ return self.poster_path
232
+
233
+ def get_imdb(self) -> str | None:
234
+ # Get remote ids (IMDB) only from tvdb
235
+ pass
236
+
237
+ def get_translations(self) -> list[str] | None:
238
+ # Get translations list only from tvdb
239
+ pass
240
+
241
+
242
+ @dataclass(slots=True)
243
+ class MovieReleaseInfo:
244
+ """
245
+ Represents release information for a movie in a specific country.
246
+ """
247
+
248
+ iso_3166_1: str | None = None
249
+ release_dates: list[dict[str, Any]] = field(default_factory=list)
250
+
251
+ def __repr__(self) -> str:
252
+ """
253
+ Returns the MovieReleaseInfo string
254
+ """
255
+ return f"<ReleaseInfo iso_3166_1={self.iso_3166_1}, release_dates={self.release_dates}>"
256
+
257
+ @classmethod
258
+ def validate(cls, data: dict) -> MovieReleaseInfo | None:
259
+ """
260
+ Validates the data; return None if it's invalid
261
+ """
262
+
263
+ iso_3166_1 = data.get("iso_3166_1")
264
+ release_dates = data.get("release_dates", {})
265
+ logger = get_logger(cls.__name__)
266
+
267
+ # Validate country code
268
+ if iso_3166_1 is not None:
269
+ if (
270
+ not isinstance(iso_3166_1, str)
271
+ or len(iso_3166_1) != 2
272
+ or not iso_3166_1.isupper()
273
+ ):
274
+ logger.error(f"Invalid ISO 3166-1 code: {iso_3166_1}")
275
+ return None
276
+
277
+ # Validate release_dates
278
+ if not isinstance(release_dates, list):
279
+ logger.error("release_dates must be a list.")
280
+ return None
281
+
282
+ for item in release_dates:
283
+ if not isinstance(item, dict):
284
+ logger.error(f"Invalid item in release_dates list: {item}")
285
+ return None
286
+
287
+ return cls(iso_3166_1=iso_3166_1, release_dates=release_dates)
models/tv.py ADDED
@@ -0,0 +1,266 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Same as the old code unit3dup 0.8.21
4
+ from dataclasses import dataclass, field
5
+ from models.interfaces import MediaRepoInterface
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class Alternative:
10
+ iso_3166_1: str
11
+ title: str
12
+ type: str
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class DataResponse:
17
+ id: int
18
+ results: list[Alternative]
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class CreatedBy:
23
+ credit_id: str
24
+ gender: int
25
+ id: int
26
+ name: str
27
+ original_name: str
28
+ profile_path: str | None = None
29
+
30
+
31
+ @dataclass(slots=True)
32
+ class Genre:
33
+ id: int
34
+ name: str
35
+
36
+
37
+ @dataclass(slots=True)
38
+ class LastEpisodeToAir:
39
+ air_date: str
40
+ episode_number: int
41
+ episode_type: str
42
+ id: int
43
+ name: str
44
+ overview: str
45
+ production_code: str
46
+ runtime: int
47
+ season_number: int
48
+ show_id: int
49
+ vote_average: float
50
+ vote_count: int
51
+ still_path: str | None = None
52
+
53
+
54
+ @dataclass(slots=True)
55
+ class Network:
56
+ id: int
57
+ logo_path: str
58
+ name: str
59
+ origin_country: str
60
+
61
+
62
+ @dataclass(slots=True)
63
+ class ProductionCompany:
64
+ id: int
65
+ name: str
66
+ origin_country: str
67
+ logo_path: str | None = None
68
+
69
+
70
+ @dataclass(slots=True)
71
+ class ProductionCountry:
72
+ iso_3166_1: str
73
+ name: str
74
+
75
+
76
+ @dataclass(slots=True)
77
+ class Season:
78
+ episode_count: int
79
+ id: int
80
+ name: str
81
+ overview: str
82
+ season_number: int
83
+ vote_average: float
84
+ air_date: str | None = None
85
+ poster_path: str | None = None
86
+
87
+
88
+ @dataclass(slots=True)
89
+ class SpokenLanguage:
90
+ english_name: str
91
+ iso_639_1: str
92
+ name: str
93
+
94
+
95
+ @dataclass(slots=True)
96
+ class TVShowDetails(MediaRepoInterface):
97
+
98
+ adult: bool
99
+ first_air_date: str
100
+ homepage: str
101
+ id: int
102
+ in_production: bool
103
+ last_air_date: str
104
+ last_episode_to_air: LastEpisodeToAir
105
+ name: str
106
+ number_of_episodes: int
107
+ number_of_seasons: int
108
+ original_language: str
109
+ original_name: str
110
+ overview: str
111
+ popularity: float
112
+ poster_path: str
113
+ status: str
114
+ tagline: str
115
+ type: str
116
+ vote_average: float
117
+ vote_count: int
118
+ languages: list[str] = field(default_factory=list)
119
+ genres: list[Genre] = field(default_factory=list)
120
+ backdrop_path: str | None = None
121
+ created_by: list[CreatedBy] = field(default_factory=list)
122
+ episode_run_time: list[int] = field(default_factory=list)
123
+ networks: list[Network] = field(default_factory=list)
124
+ next_episode_to_air: LastEpisodeToAir | None = None
125
+ production_companies: list[ProductionCompany] = field(default_factory=list)
126
+ production_countries: list[ProductionCountry] = field(default_factory=list)
127
+ seasons: list[Season] = field(default_factory=list)
128
+ spoken_languages: list[SpokenLanguage] = field(default_factory=list)
129
+ origin_country: list[str] = field(default_factory=list)
130
+
131
+
132
+ def get_title(self) -> str:
133
+ return self.name
134
+
135
+ def get_original(self) -> str:
136
+ return self.original_name
137
+
138
+ def get_date(self) -> str:
139
+ return self.first_air_date
140
+
141
+ def get_id(self) -> int:
142
+ return self.id
143
+
144
+ def get_translations(self) -> dict[str, str] | None:
145
+ pass
146
+
147
+ def get_imdb(self) -> str | None:
148
+ pass
149
+
150
+ def get_poster_path(self) -> str:
151
+ pass
152
+
153
+
154
+ @dataclass(slots=True)
155
+ class OnTheAir:
156
+ adult: bool | None = None
157
+ backdrop_path: str | None = None
158
+ genre_ids: list[int] | None = None
159
+ id: int | None = None
160
+ original_language: str | None = None
161
+ original_title: str | None = None
162
+ overview: str | None = None
163
+ popularity: float | None = None
164
+ poster_path: str | None = None
165
+ release_date: str | None = None
166
+ title: str | None = None
167
+ video: bool | None = None
168
+ vote_average: float | None = None
169
+ vote_count: int | None = None
170
+ origin_country: list[str] | None = None
171
+ original_name: str | None = None
172
+ first_air_date: str | None = None
173
+ name: str | None = None
174
+
175
+
176
+ @dataclass(slots=True)
177
+ class Translation:
178
+ """
179
+ Represents a TV show translation
180
+ """
181
+ iso_639_1: str
182
+ """
183
+ Language code of the translation
184
+ """
185
+ english_name: str
186
+ """
187
+ English name of the language
188
+ """
189
+ name: str
190
+ """
191
+ Name of the translation
192
+ """
193
+ url: str | None
194
+ """
195
+ Optional URL associated with the translation
196
+ """
197
+ tagline: str | None
198
+ """
199
+ Optional tagline for the translation
200
+ """
201
+ description: str | None
202
+ """
203
+ Optional description for the translation
204
+ """
205
+ country: str | None
206
+ """
207
+ Optional country code related to the translation
208
+ """
209
+
210
+
211
+ @dataclass(slots=True)
212
+ class TranslationsResponse:
213
+ """
214
+ Contains a list of TV show translations
215
+ """
216
+ translations: list[Translation]
217
+ """
218
+ List of Translation objects
219
+ """
220
+
221
+
222
+ @dataclass(slots=True)
223
+ class TvShow(MediaRepoInterface):
224
+ """
225
+ Dataclass for manage result from a TMDB search
226
+ """
227
+
228
+ id: int
229
+ name: str
230
+ first_air_date: str
231
+ overview: str
232
+ popularity: float
233
+ vote_average: float
234
+ vote_count: int
235
+ genre_ids: list[int] = field(default_factory=list)
236
+ origin_country: list[str] = field(default_factory=list)
237
+ original_language: str = ''
238
+ original_name: str = ''
239
+ backdrop_path: str | None = None
240
+ poster_path: str | None = None
241
+ adult: bool = False
242
+ softcore: str | None = None
243
+
244
+
245
+ def get_title(self) -> str:
246
+ return self.name
247
+
248
+ def get_original(self) -> str:
249
+ return self.original_name
250
+
251
+ def get_date(self) -> str:
252
+ return self.first_air_date
253
+
254
+ def get_id(self) -> int:
255
+ return self.id
256
+
257
+ def get_poster_path(self) -> str:
258
+ return self.poster_path
259
+
260
+ def get_imdb(self) -> str | None:
261
+ # Get remote ids (IMDB) only from tvdb
262
+ pass
263
+
264
+ def get_translations(self) -> list[str] | None:
265
+ # Get translations list only from tvdb
266
+ pass