### Create a video from name with Video.fromname Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Creates a video object from a filename using Video.fromname. The example uses an episode filename and shows the resulting Episode object. ```python >>> video = Video.fromname('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4') >>> video ``` -------------------------------- ### Get subtitle matches with get_matches Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Shows how to get the matching attributes between a subtitle and a video using get_matches. The output lists the matched fields for each subtitle. ```python >>> for s in subtitles[video]: ... sorted(s.get_matches(video)) ['country', 'episode', 'release_group', 'season', 'series', 'source', 'video_codec', 'year'] ['country', 'episode', 'season', 'series', 'source', 'year'] ``` -------------------------------- ### Import subliminal and babelfish Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Starts a session by importing the required modules. The wildcard imports bring in the core subliminal API and babelfish language support. ```python >>> import os >>> from babelfish import * >>> from subliminal import * ``` -------------------------------- ### TVDBClient language and token properties Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Properties for getting and setting the language header, and for retrieving the authentication token and checking its expiration/refresh status. ```python @property def language(self) -> str: """Header language of the response.""" return str(self.session.headers['Accept-Language']) @language.setter def language(self, value: str) -> None: self.session.headers['Accept-Language'] = value @property def token(self) -> str | None: """Authentication token.""" if 'Authorization' not in self.session.headers: return None return str(self.session.headers['Authorization'][7:]) @property def token_expired(self) -> bool: """Check if the token expired.""" return datetime.now(timezone.utc) - self.token_date >= self.token_lifespan @property def token_needs_refresh(self) -> bool: """Check if the token needs to be refreshed.""" return datetime.now(timezone.utc) - self.token_date > self.refresh_token_every ``` -------------------------------- ### Download subtitle content with download_subtitles Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Downloads the content of a subtitle using download_subtitles. The example shows that the content is initially None, then after download it contains the subtitle text. ```python >>> subtitle = subtitles[video][0] >>> subtitle.content is None True >>> download_subtitles([subtitle]) >>> subtitle.content.split(b'\n')[2] b'Elszaladok a boltba' ``` -------------------------------- ### Download best subtitles with download_best_subtitles Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Downloads the best matching subtitles using download_best_subtitles, which combines listing, scoring, and downloading. The example shows the same subtitle is selected as in the manual process. ```python >>> best_subtitles = download_best_subtitles([video], {Language('hun')}, providers=['podnapisi']) >>> best_subtitles[video] [] >>> best_subtitle = best_subtitles[video][0] >>> best_subtitle.content.split(b'\n')[2] b'Elszaladok a boltba' ``` -------------------------------- ### List subtitles with list_subtitles Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Lists subtitles for a video using the list_subtitles function. The example filters to the 'podnapisi' provider and returns a list of PodnapisiSubtitle objects. ```python >>> subtitles = list_subtitles([video], {Language('hun')}, providers=['podnapisi']) >>> subtitles[video] [, ] ``` -------------------------------- ### Save subtitles with save_subtitles Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Saves the downloaded subtitle to the file system using save_subtitles. The example shows the saved file name in the current directory. ```python >>> save_subtitles(video, [best_subtitle]) [] >>> os.listdir() ['The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.hu.srt'] ``` -------------------------------- ### Compute subtitle score with compute_score Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Computes a score for each subtitle using compute_score. The example shows scores of 789 and 772 for the two subtitles. ```python >>> for s in subtitles[video]: ... {s: compute_score(s, video)} {: 789} {: 772} ``` -------------------------------- ### Default TVDB client and guessit configuration Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Creates the default TVDBClient instance and configures guessit to use GuessitCountryConverter. ```python #: Default client tvdb_client = TVDBClient() #: Configure guessit in order to use GuessitCountryConverter guessit.api.configure() ``` -------------------------------- ### TVDBClient class definition and __init__ Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Defines the TVDBClient class with class-level constants and the constructor that sets up the API key, credentials, session, and headers. ```python class TVDBClient: """TVDB REST API Client. :param str apikey: API key to use. :param str username: username to use. :param str password: password to use. :param str language: language of the responses. :param session: session object to use. :type session: :class:`requests.sessions.Session` or compatible. :param dict headers: additional headers. :param int timeout: timeout for the requests. """ #: Base URL of the API base_url: ClassVar[str] = 'https://api.thetvdb.com' #: User agent user_agent: ClassVar[str] = f'Subliminal/{__short_version__}' #: Token lifespan token_lifespan: ClassVar[timedelta] = timedelta(hours=1) #: API version apiversion: ClassVar[int] = 1 #: Minimum token age before a :meth:`refresh_token` is triggered refresh_token_every: ClassVar[timedelta] = timedelta(minutes=30) #: API key _apikey: str #: Username username: str | None #: Password password: str | None #: Last token acquisition date token_date: datetime #: Session for the requests session: requests.Session #: Session timeout timeout: int def __init__( self, apikey: str | None = None, username: str | None = None, password: str | None = None, language: str = 'en', session: requests.Session | None = None, headers: dict | None = None, timeout: int = 10, ) -> None: self._apikey = apikey or TVDB_API_KEY self.username = username self.password = password self.token_date = datetime.now(timezone.utc) - self.token_lifespan self.timeout = timeout #: Session for the requests self.session = session if session is not None else requests.Session() self.session.headers['User-Agent'] = self.user_agent self.session.headers.update(headers or {}) self.session.headers['Content-Type'] = 'application/json' self.session.headers['Accept-Language'] = language ``` -------------------------------- ### subliminal command usage and options Source: https://subliminal.readthedocs.io/en/latest/user/cli.html Shows the top-level `subliminal` CLI usage, including global options like `--config`, `--cache-dir`, provider/refiner configurations, and available subcommands (`cache`, `download`). ```text Usage: subliminal [OPTIONS] COMMAND [ARGS]... Subtitles, faster than your thoughts. Options: -c, --config FILE Path to the TOML configuration file. [default: /home/docs/.config/subliminal/subl iminal.toml] --cache-dir DIRECTORY Path to the cache directory. [default: /home/docs/.cache/subliminal] Providers configuration: --addic7ed USERNAME PASSWORD Addic7ed configuration. --opensubtitles USERNAME PASSWORD OpenSubtitles configuration. --opensubtitlescom USERNAME PASSWORD OpenSubtitles.com configuration. Refiners configuration: --omdb APIKEY OMDB API key. --debug Print useful information for debugging subliminal and for reporting bugs. --version Show the version and exit. --help Show this message and exit. Commands: cache Cache management. download Download best subtitles. Suggestions and bug reports are greatly appreciated: https://github.com/Diaoul/subliminal/ ``` -------------------------------- ### get_series_episodes method Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Retrieves all episodes of a series with pagination. Returns an empty dictionary on 404. ```python @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) @requires_auth def get_series_episodes(self, series_id: int, page: int = 1) -> dict[str, Any]: """Get all the episodes of a series. :param int series_id: id of the series. :param int page: the page number. :return: the data for all the episodes. :rtype: dict """ # perform the request params = {'page': page} r = self.session.get(self.base_url + f'/series/{series_id:d}/episodes', params=params, timeout=self.timeout) if r.status_code == 404: return {} r.raise_for_status() return cast(dict, r.json()) ``` -------------------------------- ### get_episode method Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Retrieves episode data by episode ID. Returns an empty dictionary on 404. ```python @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) @requires_auth def get_episode(self, episode_id: int) -> dict[str, Any]: """Get episode. :param int episode_id: id of the episode. :return: the episode data. :rtype: dict """ # perform the request r = self.session.get(self.base_url + f'/episodes/{episode_id:d}', timeout=self.timeout) if r.status_code == 404: return {} r.raise_for_status() return cast(dict, r.json()['data']) ``` -------------------------------- ### subliminal download command usage and options Source: https://subliminal.readthedocs.io/en/latest/user/cli.html Shows the `subliminal download` subcommand usage, including required `--language`, provider/refiner options, and flags like `--force`, `--hearing-impaired`, and `--archives`. ```text Usage: subliminal download [OPTIONS] PATH... Download best subtitles. PATH can be an directory containing videos, a video file path or a video file name. It can be used multiple times. If an existing subtitle is detected (external or embedded) in the correct language, the download is skipped for the associated video. Options: -l, --language LANGUAGE Language as IETF code, e.g. en, pt-BR (can be used multiple times). [required] -p, --provider [addic7ed|gestdown|napiprojekt|opensubtitles|opensubtitlescom|opensubtitlescomvip|opensubtitlesvip|podnapisi|tvsubtitles] Provider to use (can be used multiple times). -r, --refiner [hash|metadata|omdb|tmdb|tvdb] Refiner to use (can be used multiple times). -a, --age AGE Filter videos newer than AGE, e.g. 12h, 1w2d. -d, --directory DIR Directory where to save subtitles, default is next to the video file. -e, --encoding ENC Force subtitle file encoding, default is utf-8. --original-encoding Preserve original subtitle file encoding. -s, --single Save subtitle without language code in the file name, i.e. use .srt extension. Do not use this unless your media player requires it. -f, --force Force download even if a subtitle already exist. -hi, --hearing-impaired Prefer hearing impaired subtitles. -m, --min-score INTEGER RANGE Minimum score for a subtitle to be downloaded (0 to 100). [0<=x<=100] -w, --max-workers INTEGER RANGE Maximum number of threads to use. [1<=x<=50] -z, --archives / -Z, --no-archives Scan archives for videos (supported extensions: .rar). [default: z] -v, --verbose Increase verbosity. --help Show this message and exit. ``` -------------------------------- ### Download English subtitles with subliminal CLI Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Downloads English subtitles for a video file. The output shows progress and confirms one subtitle was downloaded. ```bash $ subliminal download -l en The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4 Collecting videos [####################################] 100% 1 video collected / 0 video ignored Downloading subtitles [####################################] 100% Downloaded 1 subtitle ``` -------------------------------- ### subliminal.refiners.refine(video, **kwargs) Source: https://subliminal.readthedocs.io/en/latest/api/refiners.html The main entry point for refining a video. It applies all available refiners to enrich the video object with metadata. ```APIDOC ## subliminal.refiners.refine(video, **kwargs) ### Description Refines a video by applying all available refiners to enrich it with additional information. ### Parameters - **video** (`Video`) - Required - The video to refine. - **kwargs** - Optional - Additional parameters for refiners. ### Returns - `Video` - The refined video object. ``` -------------------------------- ### query_series_episodes method Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Queries episodes of a series with optional filters like absolute number, aired/dvd season/episode, IMDB ID, and page. Returns an empty dictionary on 404. ```python @requires_auth def query_series_episodes( self, series_id: int, absolute_number: int | None = None, aired_season: int | None = None, aired_episode: int | None = None, dvd_season: int | None = None, dvd_episode: int | None = None, imdb_id: str | None = None, page: int = 1, ) -> dict[str, Any]: """Query series episodes.""" # perform the request params = { 'absoluteNumber': absolute_number, 'airedSeason': aired_season, 'airedEpisode': aired_episode, 'dvdSeason': dvd_season, 'dvdEpisode': dvd_episode, 'imdbId': imdb_id, 'page': page, } r = self.session.get( self.base_url + f'/series/{series_id:d}/episodes/query', params=params, timeout=self.timeout, ) if r.status_code == 404: return {} r.raise_for_status() return cast(dict, r.json()) ``` -------------------------------- ### list_subtitles(video, languages) Source: https://subliminal.readthedocs.io/en/latest/api/providers.html Lists subtitles for a given video and set of languages. Returns a list of Subtitle objects and raises ProviderError on failure. ```APIDOC ## list_subtitles(video, languages) ### Description Lists subtitles for a given video and set of languages. ### Method list_subtitles ### Parameters - **video** (Video) - Required - The video to list subtitles for. - **languages** (set of Language) - Required - The languages to search for. ### Returns - list of Subtitle - The found subtitles. ### Raises - ProviderError - If an error occurs during the operation. ``` -------------------------------- ### Configure cache with dogpile.cache.memory Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Configures the subliminal cache using a memory backend. The documentation warns to prefer persistent backends like 'file' for production use. ```python >>> my_region = region.configure('dogpile.cache.memory') ``` -------------------------------- ### TVDBClient login method Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Performs a login request to the TheTVDB API, sets the Authorization header with the returned token, and updates the token date. ```python def login(self) -> None: """Login.""" # perform the request data = {'apikey': self.apikey, 'username': self.username, 'password': self.password} r = self.session.post(self.base_url + '/login', json=data, timeout=self.timeout) r.raise_for_status() # set the Authorization header self.session.headers['Authorization'] = 'Bearer ' + r.json()['token'] # update token_date self.token_date = datetime.now(timezone.utc) ``` -------------------------------- ### search_series method Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Searches for a series by name, optionally filtering by IMDB ID or Zap2it ID. Returns an empty dictionary on 404. ```python @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) @requires_auth def search_series(self, name: str, imdb_id: str | None = None, zap2it_id: str | None = None) -> dict[str, Any]: """Search series. :param str name: name of the series. :param str imdb_id: the IMDB id of the series. :param str zap2it_id: the Zap2it id of the series. :return: the search results. :rtype: list """ # perform the request params = {'name': name} if imdb_id is not None: params['imdbId'] = imdb_id if zap2it_id is not None: params['zap2itId'] = zap2it_id r = self.session.get(self.base_url + '/search/series', params=params, timeout=self.timeout) if r.status_code == 404: return {} r.raise_for_status() return cast(dict, r.json()['data']) ``` -------------------------------- ### subliminal.refiners.tvdb.refine(video, *, apikey=None, force=False, **kwargs) Source: https://subliminal.readthedocs.io/en/latest/api/refiners.html Refines a video by searching TheTVDB. Only works for instances of Episode. Can find series, year, series_imdb_id, series_tvdb_id, title, imdb_id, and tvdb_id. ```APIDOC ## subliminal.refiners.tvdb.refine(video, *, apikey=None, force=False, **kwargs) ### Description Refines a video by searching TheTVDB. Only works for instances of `Episode`. ### Parameters - **video** (`Video`) - Required - The video to refine. - **apikey** (`str | None`) - Optional - API key for TheTVDB. - **force** (`bool`) - Optional - Force refinement even if already refined. Defaults to `False`. - **kwargs** - Optional - Additional parameters. ### Returns - `Video` - The refined video object. ### Attributes Found - `series` - `year` - `series_imdb_id` - `series_tvdb_id` - `title` - `imdb_id` - `tvdb_id` ``` -------------------------------- ### Access video attributes Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Shows how to access guessed video attributes such as video_codec and release_group. ```python >>> video.video_codec 'H.264' >>> video.release_group 'LOL' ``` -------------------------------- ### TVDB refiner module imports and constants Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Imports required libraries, defines the TVDB API key, and sets up a regex for parsing series names. This is the initial part of the module source. ```python """Refine the :class:`~subliminal.video.Video` object by searching on TheTVDB.""" from __future__ import annotations import logging import re from datetime import datetime, timedelta, timezone from functools import wraps from typing import Any, Callable, ClassVar, TypeVar, cast import guessit # type: ignore[import-untyped] import requests from babelfish import Country # type: ignore[import-untyped] from subliminal import __short_version__ from subliminal.cache import REFINER_EXPIRATION_TIME, region from subliminal.utils import decorate_imdb_id, sanitize, sanitize_id from subliminal.video import Episode, Video C = TypeVar('C', bound=Callable) logger = logging.getLogger(__name__) #: TheTVDB subliminal API key TVDB_API_KEY = '5EC930FB90DA1ADA' series_re = re.compile(r'^(?P.*?)(?: \\((?:(?P\d{4})|(?P[A-Z]{2}))\\))?$') ``` -------------------------------- ### TMDBClient query method Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tmdb.html Queries a movie or series by its TMDB ID. Uses caching and returns a dictionary of the response. ```python @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) def query( self, tmdb_id: int, is_movie: bool, # noqa: FBT001 season: int | None = None, episode: int | None = None, ) -> dict[str, Any]: """Query a movie or series by TMDB id.""" category = 'movie' if is_movie else 'tv' logger.info('Searching for TMDB id %d (%s)', tmdb_id, category) # make url path path = f'/{category}/{tmdb_id}' if season is not None: path = f'{path}/season/{season}' if episode is not None: path = f'{path}/episode/{episode}' params: dict[str, Any] = {'append_to_response': 'external_ids'} r = self.session.get(self.base_url + path, params=params) r.raise_for_status() return cast(dict, r.json()) ``` -------------------------------- ### download_subtitle(subtitle) Source: https://subliminal.readthedocs.io/en/latest/api/providers.html Downloads a specific subtitle. Raises ProviderError on failure. ```APIDOC ## download_subtitle(subtitle) ### Description Downloads a specific subtitle. ### Method download_subtitle ### Parameters - **subtitle** (Subtitle) - Required - The subtitle to download. ### Raises - ProviderError - If an error occurs during the operation. ``` -------------------------------- ### get_series method Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Retrieves series data by series ID. Returns an empty dictionary on 404. ```python @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) @requires_auth def get_series(self, series_id: int) -> dict[str, Any]: """Get series. :param int series_id: id of the series. :return: the series data. :rtype: dict """ # perform the request r = self.session.get(self.base_url + f'/series/{series_id:d}', timeout=self.timeout) if r.status_code == 404: return {} r.raise_for_status() return cast(dict, r.json()['data']) ``` -------------------------------- ### Access decoded subtitle text with text property Source: https://subliminal.readthedocs.io/en/latest/user/usage.html Shows how to access the decoded subtitle content as a string using the text property. ```python >>> subtitle.text.split('\n')[3] 'néhány apróságért.' ``` -------------------------------- ### download_subtitle() (in module subliminal.providers) Source: https://subliminal.readthedocs.io/en/latest/genindex.html Downloads a subtitle from a provider. This function is part of the subliminal.providers module and is used to fetch subtitle content. ```APIDOC ## download_subtitle() ### Description Downloads a subtitle from a provider. ### Module subliminal.providers ### Usage Call this function to download a subtitle. ``` -------------------------------- ### subliminal.refiners.tmdb.refine(video, *, apikey=None, force=False, **kwargs) Source: https://subliminal.readthedocs.io/en/latest/api/refiners.html Refines a video by searching the TMDB API. Can find attributes for both Episode and Movie objects. ```APIDOC ## subliminal.refiners.tmdb.refine(video, *, apikey=None, force=False, **kwargs) ### Description Refines a video by searching the TMDB API. ### Parameters - **video** (`Video`) - Required - The video to refine. - **apikey** (`str | None`) - Optional - API key for TMDB. - **force** (`bool`) - Optional - Force refinement even if already refined. Defaults to `False`. - **kwargs** - Optional - Additional parameters. ### Returns - `Video` - The refined video object. ### Attributes Found for Episode - `series` - `year` - `series_tmdb_id` - `tmdb_id` - `series_imdb_id` - `imdb_id` ### Attributes Found for Movie - `title` - `year` - `tmdb_id` - `imdb_id` ``` -------------------------------- ### subliminal cache command usage and options Source: https://subliminal.readthedocs.io/en/latest/user/cli.html Shows the `subliminal cache` subcommand usage, with the `--clear-subliminal` option to clear the cache only if corrupted or experiencing issues. ```text Usage: subliminal cache [OPTIONS] Cache management. Options: --clear-subliminal Clear subliminal cache. Use this ONLY if your cache is corrupted or if you experience issues. --help Show this message and exit. ``` -------------------------------- ### TMDBClient search methods Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tmdb.html Methods to search for movies, series, or specific episodes by title and optional metadata. ```python @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) def search_movie( self, title: str, year: int | None = None, country: str | None = None, ) -> dict[str, Any]: """Search for series.""" tmdb_id = self.get_id(title, is_movie=True, year=year, country=country) if tmdb_id is None: return {} res = self.query(tmdb_id, is_movie=True) return cast(dict, res) ``` ```python @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) def search_series( self, series_name: str, year: int | None = None, country: str | None = None, ) -> dict[str, Any]: """Search for series.""" tmdb_id = self.get_id(series_name, is_movie=False, year=year, country=country) if tmdb_id is None: return {} res = self.query(tmdb_id, is_movie=False) return cast(dict, res) ``` ```python @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) def search_episode( self, series_name: str, season: int, episode: int, year: int | None = None, country: str | None = None, ) -> dict[str, Any]: """Search for episode.""" tmdb_id = self.get_id(series_name, is_movie=False, year=year, country=country) if tmdb_id is None: return {} res = self.query(tmdb_id, is_movie=False, season=season, episode=episode) return cast(dict, res) ``` -------------------------------- ### subliminal.refiners.omdb.refine(video, *, apikey=None, force=False, **kwargs) Source: https://subliminal.readthedocs.io/en/latest/api/refiners.html Refines a video by searching the OMDb API. Can find attributes for both Episode and Movie objects. ```APIDOC ## subliminal.refiners.omdb.refine(video, *, apikey=None, force=False, **kwargs) ### Description Refines a video by searching the OMDb API. ### Parameters - **video** (`Video`) - Required - The video to refine. - **apikey** (`str | None`) - Optional - API key for OMDb. - **force** (`bool`) - Optional - Force refinement even if already refined. Defaults to `False`. - **kwargs** - Optional - Additional parameters. ### Returns - `Video` - The refined video object. ### Attributes Found for Episode - `series` - `year` - `series_imdb_id` ### Attributes Found for Movie - `title` - `year` - `imdb_id` ``` -------------------------------- ### get_series_episode method Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Retrieves a specific episode of a series by season and episode number, using query_series_episodes and get_episode. Returns an empty dictionary if no result. ```python @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) @requires_auth def get_series_episode(self, series_id: int, season: int, episode: int) -> dict[str, Any]: """Get an episode of a series. :param int series_id: id of the series. :param int season: season number of the episode. :param int episode: episode number of the episode. :return: the episode data. :rtype: dict """ result = self.query_series_episodes(series_id, aired_season=season, aired_episode=episode) if not result: return {} return self.get_episode(result['data'][0]['id']) # type: ignore[no-any-return] ``` -------------------------------- ### refine(video, *, embedded_subtitles=True, **kwargs) Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/metadata.html Refines a Video object by reading its MKV metadata. It extracts resolution, video codec, audio codec, and optionally embedded subtitle languages. The function returns the modified Video object. ```APIDOC ## refine(video, *, embedded_subtitles=True, **kwargs) ### Description Refines a Video object by searching its metadata. Several Video attributes can be found: resolution, video_codec, audio_codec, and subtitle_languages. ### Method Function ### Signature refine(video: Video, *, embedded_subtitles: bool = True, **kwargs: Any) -> Video ### Parameters - **video** (Video) - Required - The Video object to refine. - **embedded_subtitles** (bool) - Optional - Whether to search for embedded subtitles. Defaults to True. - **kwargs** (Any) - Optional - Additional keyword arguments (unused). ### Returns - **Video** - The refined Video object with updated metadata attributes. ### Behavior - If the video does not exist, it is returned unchanged. - Only MKV files are processed; other extensions are ignored. - Extracts resolution (480p, 720p, 1080p, or interlaced variants) from the first video track. - Extracts video codec (H.264, DivX, Xvid) from the first video track. - Extracts audio codec (Dolby Digital, DTS, AAC) from the first audio track. - If embedded_subtitles is True, extracts subtitle languages from subtitle tracks and adds them to video.subtitle_languages. ``` -------------------------------- ### list_subtitles() (in module subliminal.providers) Source: https://subliminal.readthedocs.io/en/latest/genindex.html Lists available subtitles from a provider. This function is part of the subliminal.providers module and is used to retrieve a list of subtitles matching given criteria. ```APIDOC ## list_subtitles() ### Description Lists available subtitles from a provider. ### Module subliminal.providers ### Usage Call this function to retrieve a list of subtitles. ``` -------------------------------- ### subliminal.refiners.metadata.refine(video, *, embedded_subtitles=True, **kwargs) Source: https://subliminal.readthedocs.io/en/latest/api/refiners.html Refines a video by searching its metadata. Can find attributes such as resolution, video codec, audio codec, and subtitle languages. ```APIDOC ## subliminal.refiners.metadata.refine(video, *, embedded_subtitles=True, **kwargs) ### Description Refines a video by searching its metadata. Can find attributes such as resolution, video codec, audio codec, and subtitle languages. ### Parameters - **video** (`Video`) - Required - The video to refine. - **embedded_subtitles** (`bool`) - Optional - Search for embedded subtitles. Defaults to `True`. - **kwargs** - Optional - Additional parameters. ### Returns - `Video` - The refined video object. ### Attributes Found - `resolution` - `video_codec` - `audio_codec` - `subtitle_languages` ``` -------------------------------- ### refine function for TheTVDB Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Refines an Episode video by searching TheTVDB. Only works for Episode instances; logs an error and returns for movies. Exits early if series_tvdb_id and tvdb_id already exist unless force is True. Updates the API key if provided, searches for the series, and filters results by year and country. ```python def refine(video: Video, *, apikey: str | None = None, force: bool = False, **kwargs: Any) -> Video: """Refine a video by searching `TheTVDB `_. .. note:: This refiner only work for instances of :class:`~subliminal.video.Episode`. Several attributes can be found: * :attr:`~subliminal.video.Episode.series` * :attr:`~subliminal.video.Episode.year` * :attr:`~subliminal.video.Episode.series_imdb_id` * :attr:`~subliminal.video.Episode.series_tvdb_id` * :attr:`~subliminal.video.Episode.title` * :attr:`~subliminal.video.Video.imdb_id` * :attr:`~subliminal.video.Episode.tvdb_id` """ # only deal with Episode videos if not isinstance(video, Episode): logger.error('Cannot refine movies') return video # exit if the information is complete if not force and video.series_tvdb_id and video.tvdb_id: logger.debug('No need to search, TheTVDB ids already exist for the video.') return video # update the API key if apikey is not None: tvdb_client.apikey = apikey # search the series logger.info('Searching series %r', video.series) results = tvdb_client.search_series(video.series.lower()) if not results: logger.warning('No results for series') return video logger.debug('Found %d results', len(results)) # search for exact matches matching_results = [] for result in results: matching_result = {} # use seriesName and aliases original_series_name = result['seriesName'] series_names = [original_series_name, *result['aliases']] # parse the original series as series + year or country series_match = series_re.match(original_series_name) if not series_match: # pragma: no-cover logger.debug('Discarding series %r, cannot match to regex %r', original_series_name, series_re) continue original_match = series_match.groupdict() # parse series year series_year = None if result['firstAired']: first_aired = datetime.strptime(result['firstAired'], '%Y-%m-%d').replace(tzinfo=timezone.utc) series_year = first_aired.year # discard mismatches on year if video.year and series_year and video.year != series_year: logger.debug('Discarding series %r mismatch on year %d', result['seriesName'], series_year) continue # iterate over series names for series_name in series_names: # parse as series, year and country series_match = series_re.match(series_name) if not series_match: # pragma: no-cover logger.debug('Discarding series name %r, cannot match to regex %r', series_name, series_re) continue series, year, country = series_match.groups() if year: year = int(year) if country: country = Country.fromguessit(country) # discard mismatches on year if year and (video.original_series or video.year != year): logger.debug('Discarding series name %r mismatch on year %d', series, year) continue # discard mismatches on country if video.country and video.country != country: logger.debug('Discarding series name %r mismatch on country %r', series, country) continue ``` -------------------------------- ### TVDB refiner: match series and episode Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Refines a video by matching its series name and retrieving episode data from TheTVDB. It logs errors when no or multiple matches are found, and updates video fields with series and episode metadata. ```python # match on sanitized series name if sanitize(series) == sanitize(video.series): logger.debug('Found exact match on series %r', series_name) matching_result['match'] = { 'series': original_match.get('series', series), 'year': series_year or year, 'country': country, 'original_series': original_match.get('year') is None and country is None, } break # add the result on match if matching_result: matching_result['data'] = result matching_results.append(matching_result) # exit if we don't have exactly 1 matching result if not matching_results: logger.error('No matching series found') return video if len(matching_results) > 1: logger.error('Multiple matches found') return video # get the series matching_result = matching_results[0] series = tvdb_client.get_series(matching_result['data']['id']) # add series information logger.debug('Found series %r', series) video.series = matching_result['match']['series'] video.alternative_series.extend(series['aliases']) video.year = matching_result['match']['year'] video.country = matching_result['match']['country'] video.original_series = matching_result['match']['original_series'] video.series_tvdb_id = sanitize_id(series['id']) video.series_imdb_id = decorate_imdb_id(sanitize_id(series['imdbId'] or None)) # get the episode logger.info('Getting series episode %dx%d', video.season, video.episode) episode = tvdb_client.get_series_episode(video.series_tvdb_id, video.season, video.episode) if not episode: logger.warning('No results for episode') return video # add episode information logger.debug('Found episode %r', episode) video.tvdb_id = sanitize_id(episode['id']) video.title = episode['episodeName'] or None video.imdb_id = decorate_imdb_id(sanitize_id(episode['imdbId'] or None)) return video ``` -------------------------------- ### TVDBClient apikey property and setter Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Property for the API key used for search. The setter invalidates the Authorization header when the key changes, but returns early if the key is unchanged. ```python @property def apikey(self) -> str: """API key for search.""" return self._apikey @apikey.setter def apikey(self, value: str) -> None: # early return if the API key is unchanged if value == self._apikey: return self._apikey = value # invalidate the token if 'Authorization' in self.session.headers: del self.session.headers['Authorization'] ``` -------------------------------- ### refresh_token method Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tvdb.html Refreshes the authentication token by calling the /refresh_token endpoint and updates the Authorization header and token_date. ```python def refresh_token(self) -> None: """Refresh token.""" # perform the request r = self.session.get(self.base_url + '/refresh_token', timeout=self.timeout) r.raise_for_status() # set the Authorization header self.session.headers['Authorization'] = 'Bearer ' + r.json()['token'] # update token_date self.token_date = datetime.now(timezone.utc) ``` -------------------------------- ### TMDB Module Source Code Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tmdb.html The complete source code for the TMDB refiner, including the TMDBClient class and helper functions for searching and ID retrieval. ```python """Refine the :class:`~subliminal.video.Video` object by searching on TMDB.""" from __future__ import annotations import logging import re from typing import Any, ClassVar, cast import requests from subliminal import __short_version__ from subliminal.cache import REFINER_EXPIRATION_TIME, region from subliminal.utils import decorate_imdb_id, sanitize, sanitize_id from subliminal.video import Episode, Movie, Video logger = logging.getLogger(__name__) series_re = re.compile(r'^(?P.*?)(?: \((?:(?P\d{4})|(?P[A-Z]{2}))\))?$') def split_year(data: dict[str, Any], key: str) -> int | None: """Found the date from dict and split the year.""" try: return int(data[key].split('-')[0]) except (ValueError, KeyError, AttributeError): logger.exception(f'Cannot extract year from date in {data.get(key)}') return None class TMDBClient: """TMDB API Client. :param str apikey: API key to use. :param session: session object to use. :type session: :class:`requests.sessions.Session` or compatible. :param dict headers: additional headers. :param int timeout: timeout for the requests. """ #: API version api_version: ClassVar[int] = 3 #: Base URL of the API base_url: ClassVar[str] = f'https://api.themoviedb.org/{api_version}' #: User agent user_agent: ClassVar[str] = f'Subliminal/{__short_version__}' #: API key _apikey: str | None #: Session for the requests session: requests.Session #: Session timeout timeout: int def __init__( self, apikey: str | None = None, session: requests.Session | None = None, headers: dict | None = None, timeout: int = 10, ) -> None: self._apikey = apikey self.timeout = timeout #: Session for the requests self.session = session if session is not None else requests.Session() self.session.headers['User-Agent'] = self.user_agent self.session.headers.update(headers or {}) self.session.headers['Content-Type'] = 'application/json' self.session.headers['Accept-Language'] = 'en' if apikey is not None: self.session.params['api_key'] = self.apikey # type: ignore[index] @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) def search( self, title: str, is_movie: bool, # noqa: FBT001 year: int | None = None, page: int = 1, ) -> list[dict[str, Any]]: """Search on TMDB and return a list of results.""" title = title.replace("'", '') title_year = title + (f' ({year})' if year else '') category = 'movie' if is_movie else 'tv' logger.info('Searching TMDB for %r (%s)', title_year, category) params: dict[str, Any] = {'query': title, 'language': 'en-US', 'page': page} if year is not None: params['year'] = year r = self.session.get(self.base_url + f'/search/{category}', params=params) r.raise_for_status() return cast(list, r.json().get('results')) @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME) def get_id( self, title: str, is_movie: bool, # noqa: FBT001 year: int | None = None, country: str | None = None, ) -> int | None: """Search on TMDB and return the best matching.""" results = self.search(title, year=year, is_movie=is_movie) title = title.replace("'", '') sanitized_title = sanitize(title) title_key = 'title' if is_movie else 'name' year_key = 'release_date' if is_movie else 'first_air_date' # Loosely match if we found only one result, it is a very probable match loose_matching = len(results) == 1 for result in results: # match title # first try 'title' in result, fallback to 'name' res_title = sanitize(result.get(title_key)) # res_title = sanitize(result.get('title', result.get('name'), None)) if not loose_matching and res_title != sanitized_title: continue # match year if year is not None: match_year = split_year(result, year_key) if match_year is None or match_year != year: continue # match country if not loose_matching and country is not None and 'origin_country' in result: # noqa: SIM102 if country.lower() not in [c.lower() for c in result['origin_country']]: continue # found a match return int(result['id']) title_year = title + (f' ({year})' if year else '') logger.warning('No match for %r from the %d results', title_year, len(results)) return None ``` -------------------------------- ### TMDBClient apikey property Source: https://subliminal.readthedocs.io/en/latest/_modules/subliminal/refiners/tmdb.html Property to manage the API key, updating the session parameters when set. ```python @property def apikey(self) -> str | None: """API key for search.""" return self._apikey @apikey.setter def apikey(self, value: str | None) -> None: # early return if the API key is unchanged if value == self._apikey: return self._apikey = value # update the default session parameters if value: self.session.params['api_key'] = self.apikey # type: ignore[index] ```