Sponsor:

Your company here, and a link to your site. Click to find out more.

subliminal - Man Page

Examples (TL;DR)

Name

subliminal — subliminal Documentation

Subliminal is a python 2.7+ library to search and download subtitles. It comes with an easy to use yet powerful CLI suitable for direct use or cron jobs.

Documentation

Usage

CLI

Download English subtitles:

$ 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
WARNING:

For cron usage, make sure to specify a maximum age (with --age) so subtitles are searched for recent videos only. Otherwise you will get banned from the providers for abuse due to too many requests. If subliminal didn’t find subtitles for an old video, it’s unlikely it will find subtitles for that video ever anyway.

See CLI for more details on the available commands and options.

Nautilus/Nemo integration

See the dedicated project page for more information.

High level API

You can call subliminal in many different ways depending on how much control you want over the process. For most use cases, you can stick to the standard API.

Common

Let’s start by importing subliminal:

>>> import os
>>> from babelfish import *
>>> from subliminal import *

Before going further, there are a few things to know about subliminal.

Video

The Movie and Episode classes represent a video, existing or not. You can create a video by name (or path) with Video.fromname, use scan_video() on an existing file path to get even more information about the video or use scan_videos() on an existing directory path to scan a whole directory for videos.

>>> video = Video.fromname('The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.mp4')
>>> video
<Episode [The Big Bang Theory s05e18]>

Here video information was guessed based on the name of the video, you can access some video attributes:

>>> video.video_codec
'H.264'
>>> video.release_group
'LOL'

Configuration

Before proceeding to listing and downloading subtitles, you need to configure the cache. Subliminal uses a cache to reduce repeated queries to providers and improve overall performance with no impact on search quality. For the sake of this example, we’re going to use a memory backend.

>>> my_region = region.configure('dogpile.cache.memory')
WARNING:

Choose a cache that fits your application and prefer persistent over volatile backends. The file backend is usually a good choice. See dogpile.cache’s documentation for more details on backends.

Now that we’re done with the basics, let’s have some real fun.

Listing

To list subtitles, subliminal provides a list_subtitles() function that will return all found subtitles:

>>> subtitles = list_subtitles([video], {Language('hun')}, providers=['podnapisi'])
>>> subtitles[video]
[<PodnapisiSubtitle 'ZtAW' [hu]>, <PodnapisiSubtitle 'ONAW' [hu]>]
NOTE:

As you noticed, all parameters are iterables but only contain one item which means you can deal with a lot of videos, languages and providers at the same time. For the sake of this example, we filter providers to use only one, pass providers=None (default) to search on all providers.

Scoring

It’s usual you have multiple candidates for subtitles. To help you chose which one to download, subliminal can compare them to the video and tell you exactly what matches with get_matches():

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

And then compute a score with those matches with compute_score():

>>> for s in subtitles[video]:
...     {s: compute_score(s, video)}
{<PodnapisiSubtitle 'ZtAW' [hu]>: 789}
{<PodnapisiSubtitle 'ONAW' [hu]>: 772}

Now you should have a better idea about which one you should choose.

Downloading

We can settle on the first subtitle and download its content using download_subtitles():

>>> subtitle = subtitles[video][0]
>>> subtitle.content is None
True
>>> download_subtitles([subtitle])
>>> subtitle.content.split(b'\n')[2]
b'Elszaladok a boltba'

If you want a string instead of bytes, you can access decoded content with the text property:

>>> subtitle.text.split('\n')[3]
'néhány apróságért.'

Downloading best subtitles

Downloading best subtitles is what you want to do in almost all cases, as a shortcut for listing, scoring and downloading you can use download_best_subtitles():

>>> best_subtitles = download_best_subtitles([video], {Language('hun')}, providers=['podnapisi'])
>>> best_subtitles[video]
[<PodnapisiSubtitle 'ZtAW' [hu]>]
>>> best_subtitle = best_subtitles[video][0]
>>> best_subtitle.content.split(b'\n')[2]
b'Elszaladok a boltba'

We end up with the same subtitle but with one line of code. Neat.

Save

We got ourselves a nice subtitle, now we can save it on the file system using save_subtitles():

>>> save_subtitles(video, [best_subtitle])
[<PodnapisiSubtitle 'ZtAW' [hu]>]
>>> os.listdir()
['The.Big.Bang.Theory.S05E18.HDTV.x264-LOL.hu.srt']

How it works

Providers

Subliminal uses multiple providers to give users a vast choice and have a better chance to find the best matching subtitles. Current supported providers are:

  • Addic7ed
  • LegendasTV
  • NapiProjekt
  • OpenSubtitles
  • Podnapisi
  • Shooter
  • TheSubDB
  • TvSubtitles

Providers all inherit the same Provider base class and thus share the same API. They are registered on the subliminal.providers entry point and are exposed through the provider_manager for easy access.

To work with multiple providers seamlessly, the ProviderPool exposes the same API but distributes it to its providers and AsyncProviderPool does it asynchronously.

Scoring

Rating subtitles and comparing them is probably the most difficult part and this is where subliminal excels with its powerful scoring algorithm.

Using guessit and enzyme, subliminal extracts properties of the video and match them with the properties of the subtitles found with the providers.

Equations in subliminal.score give a score to each property (called a match). The more matches the video and the subtitle have, the higher the score computed with compute_score() gets.

Libraries

Various libraries are used by subliminal and are key to its success:

  • guessit to guess information from filenames
  • enzyme to detect embedded subtitles in videos and read other video metadata
  • babelfish to work with languages
  • requests to make human readable HTTP requests
  • BeautifulSoup to parse HTML and XML
  • dogpile.cache to cache intermediate search results
  • stevedore to manage the provider entry point
  • chardet to detect subtitles’ encoding
  • pysrt to validate downloaded subtitles

CLI

subliminal

Traceback (most recent call last):
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/bin/subliminal", line 33, in <module>
    sys.exit(load_entry_point('subliminal==2.1.0', 'console_scripts', 'subliminal')())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/bin/subliminal", line 25, in importlib_load_entry_point
    return next(matches).load()
           ^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/importlib/metadata/__init__.py", line 205, in load
    module = import_module(match.group('module'))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 994, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/lib/python3.12/site-packages/subliminal/__init__.py", line 11, in <module>
    from .core import (AsyncProviderPool, ProviderPool, check_video, download_best_subtitles, download_subtitles,
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/lib/python3.12/site-packages/subliminal/core.py", line 11, in <module>
    from babelfish import Language, LanguageReverseError
  File "/usr/lib/python3.12/site-packages/babelfish/__init__.py", line 14, in <module>
    from .converters import (LanguageConverter, LanguageReverseConverter, LanguageEquivalenceConverter, CountryConverter,
  File "/usr/lib/python3.12/site-packages/babelfish/converters/__init__.py", line 5, in <module>
    from pkg_resources import iter_entry_points, EntryPoint
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 3322, in <module>
    @_call_aside
     ^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 3297, in _call_aside
    f(*args, **kwargs)
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 3335, in _initialize_master_working_set
    working_set = WorkingSet._build_master()
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 626, in _build_master
    ws.require(__requires__)
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 963, in require
    needed = self.resolve(parse_requirements(requirements))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 824, in resolve
    dist = self._resolve_dist(
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 865, in _resolve_dist
    raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'pytz>=2012c' distribution was not found and is required by subliminal

subliminal download

Traceback (most recent call last):
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/bin/subliminal", line 33, in <module>
    sys.exit(load_entry_point('subliminal==2.1.0', 'console_scripts', 'subliminal')())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/bin/subliminal", line 25, in importlib_load_entry_point
    return next(matches).load()
           ^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/importlib/metadata/__init__.py", line 205, in load
    module = import_module(match.group('module'))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 994, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/lib/python3.12/site-packages/subliminal/__init__.py", line 11, in <module>
    from .core import (AsyncProviderPool, ProviderPool, check_video, download_best_subtitles, download_subtitles,
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/lib/python3.12/site-packages/subliminal/core.py", line 11, in <module>
    from babelfish import Language, LanguageReverseError
  File "/usr/lib/python3.12/site-packages/babelfish/__init__.py", line 14, in <module>
    from .converters import (LanguageConverter, LanguageReverseConverter, LanguageEquivalenceConverter, CountryConverter,
  File "/usr/lib/python3.12/site-packages/babelfish/converters/__init__.py", line 5, in <module>
    from pkg_resources import iter_entry_points, EntryPoint
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 3322, in <module>
    @_call_aside
     ^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 3297, in _call_aside
    f(*args, **kwargs)
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 3335, in _initialize_master_working_set
    working_set = WorkingSet._build_master()
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 626, in _build_master
    ws.require(__requires__)
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 963, in require
    needed = self.resolve(parse_requirements(requirements))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 824, in resolve
    dist = self._resolve_dist(
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 865, in _resolve_dist
    raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'pytz>=2012c' distribution was not found and is required by subliminal

subliminal cache

Traceback (most recent call last):
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/bin/subliminal", line 33, in <module>
    sys.exit(load_entry_point('subliminal==2.1.0', 'console_scripts', 'subliminal')())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/bin/subliminal", line 25, in importlib_load_entry_point
    return next(matches).load()
           ^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/importlib/metadata/__init__.py", line 205, in load
    module = import_module(match.group('module'))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 994, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/lib/python3.12/site-packages/subliminal/__init__.py", line 11, in <module>
    from .core import (AsyncProviderPool, ProviderPool, check_video, download_best_subtitles, download_subtitles,
  File "/builddir/build/BUILDROOT/python-subliminal-2.1.0-15.fc40.noarch/usr/lib/python3.12/site-packages/subliminal/core.py", line 11, in <module>
    from babelfish import Language, LanguageReverseError
  File "/usr/lib/python3.12/site-packages/babelfish/__init__.py", line 14, in <module>
    from .converters import (LanguageConverter, LanguageReverseConverter, LanguageEquivalenceConverter, CountryConverter,
  File "/usr/lib/python3.12/site-packages/babelfish/converters/__init__.py", line 5, in <module>
    from pkg_resources import iter_entry_points, EntryPoint
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 3322, in <module>
    @_call_aside
     ^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 3297, in _call_aside
    f(*args, **kwargs)
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 3335, in _initialize_master_working_set
    working_set = WorkingSet._build_master()
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 626, in _build_master
    ws.require(__requires__)
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 963, in require
    needed = self.resolve(parse_requirements(requirements))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 824, in resolve
    dist = self._resolve_dist(
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pkg_resources/__init__.py", line 865, in _resolve_dist
    raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'pytz>=2012c' distribution was not found and is required by subliminal

Provider Guide

This guide is going to explain how to add a Provider to subliminal. You are encouraged to take a look at the existing providers, it can be a nice base to start your own provider.

Requirements

When starting a provider you should be able to answer to the following questions:

  • What languages does my provider support?
  • What are the language codes for the supported languages?
  • Does my provider deliver subtitles for episodes? for movies?
  • Does my provider require a video hash?

Each response of these questions will help you set the correct attributes for your Provider.

Video Validation

Not all providers deliver subtitles for Episode. Some may require a hash. The check() method does validation against a Video object and will return False if the given Video isn’t suitable. If you’re not happy with the default implementation, you can override it.

Configuration

API keys must not be configurable by the user and must remain linked to subliminal. Hence they must be written in the provider module.

Per-user authentication is allowed and must be configured at instantiation as keyword arguments. Configuration will be done by the user through the provider_configs argument of the list_subtitles() and download_best_subtitles() functions. No network operation must be done during instantiation, only configuration. Any error in the configuration must raise a ConfigurationError.

Beyond this point, if an error occurs, a generic ProviderError exception must be raised. You can also use more explicit exception classes AuthenticationError and DownloadLimitExceeded.

Initialization / Termination

Actual authentication operations must take place in the initialize() method. If you need anything to be executed when the provider isn’t used anymore like logout, use terminate().

Caching policy

To save bandwidth and improve querying time, intermediate data should be cached when possible. Typical use case is when a query to retrieve show ids is required prior to the query to actually search for subtitles. In that case the function that gets the show id from the show name must be cached. Expiration time should be SHOW_EXPIRATION_TIME for shows and EPISODE_EXPIRATION_TIME for episodes.

Language

To be able to handle various language codes, subliminal makes use of babelfish Language and converters. You must set the attribute languages with a set of supported Language.

If you cannot find a suitable converter for your provider, you can make one of your own.

Querying

The query() method parameters must include all aspects of provider’s querying with primary types.

Subtitle

A custom Subtitle subclass must be created to represent a subtitle from the provider. It must have relevant attributes that can be used to compute the matches of the subtitle against a Video object.

Score computation

To be able to compare subtitles coming from different providers between them, the get_matches() method must be implemented.

Unittesting

All possible uses of query(), list_subtitles() and download_subtitle() methods must have integration tests. Use vcrpy for recording and playback of network activity. Other functions must be unittested. If necessary, you can use unittest.mock to mock some functions.

API Documentation

If you are looking for information on a specific function, class or method, this part of the documentation is for you.

Core

subliminal.core.ARCHIVE_EXTENSIONS

Supported archive extensions

Video

subliminal.video.VIDEO_EXTENSIONS

Video extensions

Subtitle

subliminal.subtitle.SUBTITLE_EXTENSIONS

Subtitle extensions

Providers

Addic7ed

LegendasTv

NapiProjekt

OpenSubtitles

Podnapisi

Shooter

TheSubDB

TVsubtitles

Refiners

Refiners enrich a Video object by adding information to it.

A refiner is a simple function:

subliminal.refiners.refine(video, **kwargs)
Parameters
  • video (Video) – the video to refine.
  • **kwargs – additional parameters for refiners.

Metadata

subliminal.refiners.metadata.refine(video, embedded_subtitles=True, **kwargs)

Refine a video by searching its metadata.

Several Video attributes can be found:

  • resolution
  • video_codec
  • audio_codec
  • subtitle_languages
Parameters

embedded_subtitles (bool) – search for embedded subtitles.

TVDB

subliminal.refiners.tvdb.refine(video, **kwargs)

Refine a video by searching TheTVDB.

NOTE:

This refiner only work for instances of Episode.

Several attributes can be found:

  • series
  • year
  • series_imdb_id
  • series_tvdb_id
  • title
  • imdb_id
  • tvdb_id

OMDb

subliminal.refiners.omdb.refine(video, apikey=None, **kwargs)

Refine a video by searching OMDb API.

Several Episode attributes can be found:

  • series
  • year
  • series_imdb_id

Similarly, for a Movie:

  • title
  • year
  • imdb_id

Extensions

Score

This module provides the default implementation of the compute_score parameter in download_best_subtitles() and download_best_subtitles().

NOTE:

To avoid unnecessary dependency on sympy and boost subliminal’s import time, the resulting scores are hardcoded here and manually updated when the set of equations change.

Available matches:

  • hash
  • title
  • year
  • country
  • series
  • season
  • episode
  • release_group
  • streaming_service
  • source
  • audio_codec
  • resolution
  • hearing_impaired
  • video_codec
  • series_imdb_id
  • imdb_id
  • tvdb_id

Utils

Cache

subliminal.cache.SHOW_EXPIRATION_TIME

Expiration time for show caching

subliminal.cache.EPISODE_EXPIRATION_TIME

Expiration time for episode caching

subliminal.cache.REFINER_EXPIRATION_TIME

Expiration time for scraper searches

subliminal.cache.region

The CacheRegion

Refer to dogpile.cache’s region configuration documentation to see how to configure the region

CLI

Subliminal uses click to provide a powerful CLI.

Exceptions

License

MIT

Author

Antoine Bertin

Info

Jan 26, 2024 2.1.0