forked from platypush/platypush
[torrent
] Updated plugin.
- The default PopcornTime API host has changed, as popcorn-time.ga is no longer available. - The iMDb API now requires a paid tier even for a basic query. The official iMDb API layer (and the API key requirement) has thus been replaced with a dear ol' scraping of the frontend endpoint. - Pass of Black/LINT.
This commit is contained in:
parent
35571b8d13
commit
4c5366849d
1 changed files with 84 additions and 108 deletions
|
@ -1,11 +1,12 @@
|
|||
import os
|
||||
import queue
|
||||
import random
|
||||
from typing import List, Optional
|
||||
|
||||
import requests
|
||||
import threading
|
||||
import time
|
||||
from urllib.parse import quote_plus
|
||||
from typing import Iterable, List, Optional, Union
|
||||
|
||||
import requests
|
||||
|
||||
from platypush.context import get_bus
|
||||
from platypush.plugins import Plugin, action
|
||||
|
@ -20,6 +21,7 @@ from platypush.message.event.torrent import (
|
|||
TorrentResumedEvent,
|
||||
TorrentQueuedEvent,
|
||||
)
|
||||
from platypush.utils import get_default_downloads_dir
|
||||
|
||||
|
||||
class TorrentPlugin(Plugin):
|
||||
|
@ -27,57 +29,36 @@ class TorrentPlugin(Plugin):
|
|||
Plugin to search and download torrents.
|
||||
"""
|
||||
|
||||
_http_timeout = 20
|
||||
|
||||
# Wait time in seconds between two torrent transfer checks
|
||||
_MONITOR_CHECK_INTERVAL = 3
|
||||
|
||||
default_torrent_ports = [6881, 6891]
|
||||
categories = {
|
||||
'movies': None,
|
||||
'tv': None,
|
||||
}
|
||||
|
||||
default_torrent_ports = (6881, 6891)
|
||||
torrent_state = {}
|
||||
transfers = {}
|
||||
# noinspection HttpUrlsUsage
|
||||
default_popcorn_base_url = 'http://popcorn-time.ga'
|
||||
default_popcorn_base_url = 'https://shows.cf'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
download_dir=None,
|
||||
torrent_ports=None,
|
||||
imdb_key=None,
|
||||
popcorn_base_url=default_popcorn_base_url,
|
||||
download_dir: Optional[str] = None,
|
||||
torrent_ports: Iterable[int] = default_torrent_ports,
|
||||
popcorn_base_url: str = default_popcorn_base_url,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
:param download_dir: Directory where the videos/torrents will be downloaded (default: none)
|
||||
:type download_dir: str
|
||||
|
||||
:param download_dir: Directory where the videos/torrents will be
|
||||
downloaded (default: ``~/Downloads``).
|
||||
:param torrent_ports: Torrent ports to listen on (default: 6881 and 6891)
|
||||
:type torrent_ports: list[int]
|
||||
|
||||
:param imdb_key: The IMDb API (https://imdb-api.com/) is used to search for movies and series. Insert your
|
||||
IMDb API key if you want support for content search.
|
||||
:type imdb_key: str
|
||||
|
||||
:param popcorn_base_url: Custom base URL to use for the PopcornTime API.
|
||||
:type popcorn_base_url: str
|
||||
"""
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if torrent_ports is None:
|
||||
torrent_ports = []
|
||||
|
||||
for category in self.categories.keys():
|
||||
self.categories[category] = getattr(self, 'search_' + category)
|
||||
|
||||
self.imdb_key = imdb_key
|
||||
self.imdb_urls = {}
|
||||
self.torrent_ports = (
|
||||
torrent_ports if torrent_ports else self.default_torrent_ports
|
||||
self.torrent_ports = torrent_ports
|
||||
self.download_dir = os.path.abspath(
|
||||
os.path.expanduser(download_dir or get_default_downloads_dir())
|
||||
)
|
||||
self.download_dir = None
|
||||
self._sessions = {}
|
||||
self._lt_session = None
|
||||
self.popcorn_base_url = popcorn_base_url
|
||||
|
@ -86,48 +67,38 @@ class TorrentPlugin(Plugin):
|
|||
'tv': f'{popcorn_base_url}/show/{{}}',
|
||||
}
|
||||
|
||||
if imdb_key:
|
||||
self.imdb_urls = {
|
||||
'movies': f'https://imdb-api.com/API/SearchMovie/{self.imdb_key}/{{}}',
|
||||
'tv': f'https://imdb-api.com/API/SearchSeries/{self.imdb_key}/{{}}',
|
||||
}
|
||||
|
||||
if download_dir:
|
||||
self.download_dir = os.path.abspath(os.path.expanduser(download_dir))
|
||||
|
||||
@action
|
||||
def search(self, query, category=None, language=None, *args, **kwargs):
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
*args,
|
||||
category: Optional[Union[str, Iterable[str]]] = None,
|
||||
language: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Perform a search of video torrents.
|
||||
|
||||
:param query: Query string, video name or partial name
|
||||
:type query: str
|
||||
|
||||
:param category: Category to search. Supported types: "movies", "tv".
|
||||
Default: None (search all categories)
|
||||
:type category: str or list
|
||||
|
||||
:param language: Language code for the results - example: "en" (default: None, no filter)
|
||||
:type language: str
|
||||
"""
|
||||
|
||||
assert self.imdb_key, 'No imdb_key specified'
|
||||
results = []
|
||||
if isinstance(category, str):
|
||||
category = [category]
|
||||
|
||||
# noinspection PyCallingNonCallable
|
||||
def worker(cat):
|
||||
if cat not in self.categories:
|
||||
raise RuntimeError(
|
||||
'Unsupported category {}. Supported category: {}'.format(
|
||||
cat, self.categories.keys()
|
||||
)
|
||||
f'Unsupported category {cat}. Supported categories: '
|
||||
f'{list(self.categories.keys())}'
|
||||
)
|
||||
|
||||
self.logger.info('Searching {} torrents for "{}"'.format(cat, query))
|
||||
self.logger.info('Searching %s torrents for "%s"', cat, query)
|
||||
results.extend(
|
||||
self.categories[cat](query, *args, language=language, **kwargs)
|
||||
self.categories[cat](self, query, *args, language=language, **kwargs)
|
||||
)
|
||||
|
||||
workers = [
|
||||
|
@ -135,29 +106,40 @@ class TorrentPlugin(Plugin):
|
|||
for category in (category or self.categories.keys())
|
||||
]
|
||||
|
||||
for worker in workers:
|
||||
worker.start()
|
||||
for worker in workers:
|
||||
worker.join()
|
||||
for wrk in workers:
|
||||
wrk.start()
|
||||
for wrk in workers:
|
||||
wrk.join()
|
||||
|
||||
return results
|
||||
|
||||
def _imdb_query(self, query: str, category: str):
|
||||
assert self.imdb_key, 'No imdb_key specified'
|
||||
imdb_url = self.imdb_urls.get(category)
|
||||
if not query:
|
||||
return []
|
||||
|
||||
assert imdb_url, f'No such category: {category}'
|
||||
imdb_url = imdb_url.format(query)
|
||||
response = requests.get(imdb_url).json()
|
||||
if category == 'movies':
|
||||
imdb_category = 'movie'
|
||||
elif category == 'tv':
|
||||
imdb_category = 'tvSeries'
|
||||
else:
|
||||
raise RuntimeError(f'Unsupported category: {category}')
|
||||
|
||||
imdb_url = f'https://v3.sg.media-imdb.com/suggestion/x/{quote_plus(query)}.json?includeVideos=1'
|
||||
response = requests.get(imdb_url, timeout=self._http_timeout)
|
||||
response.raise_for_status()
|
||||
response = response.json()
|
||||
assert not response.get('errorMessage'), response['errorMessage']
|
||||
return response.get('results', [])
|
||||
return [
|
||||
item for item in response.get('d', []) if item.get('qid') == imdb_category
|
||||
]
|
||||
|
||||
def _torrent_search_worker(self, imdb_id: str, category: str, q: queue.Queue):
|
||||
base_url = self.torrent_base_urls.get(category)
|
||||
assert base_url, f'No such category: {category}'
|
||||
try:
|
||||
results = requests.get(base_url.format(imdb_id)).json()
|
||||
results = requests.get(
|
||||
base_url.format(imdb_id), timeout=self._http_timeout
|
||||
).json()
|
||||
q.put(results)
|
||||
except Exception as e:
|
||||
q.put(e)
|
||||
|
@ -192,7 +174,7 @@ class TorrentPlugin(Plugin):
|
|||
worker.join()
|
||||
|
||||
if errors:
|
||||
self.logger.warning(f'Torrent search errors: {[str(e) for e in errors]}')
|
||||
self.logger.warning('Torrent search errors: %s', [str(e) for e in errors])
|
||||
|
||||
return results
|
||||
|
||||
|
@ -206,8 +188,9 @@ class TorrentPlugin(Plugin):
|
|||
'imdb_id': result.get('imdb_id'),
|
||||
'type': 'movies',
|
||||
'file': item.get('file'),
|
||||
'title': '{title} [movies][{language}][{quality}]'.format(
|
||||
title=result.get('title'), language=lang, quality=quality
|
||||
'title': (
|
||||
result.get('title', '[No Title]')
|
||||
+ f' [movies][{lang}][{quality}]'
|
||||
),
|
||||
'duration': int(result.get('runtime') or 0),
|
||||
'year': int(result.get('year') or 0),
|
||||
|
@ -244,12 +227,10 @@ class TorrentPlugin(Plugin):
|
|||
'type': 'tv',
|
||||
'file': item.get('file'),
|
||||
'series': result.get('title'),
|
||||
'title': '{series} [S{season:02d}E{episode:02d}] {title} [tv][{quality}]'.format(
|
||||
series=result.get('title'),
|
||||
season=episode.get('season'),
|
||||
episode=episode.get('episode'),
|
||||
title=episode.get('title'),
|
||||
quality=quality,
|
||||
'title': (
|
||||
result.get('title', '[No Title]')
|
||||
+ f'[S{episode.get("season", 0):02d}E{episode.get("episode", 0):02d}] '
|
||||
+ f'{episode.get("title", "[No Title]")} [tv][{quality}]'
|
||||
),
|
||||
'duration': int(result.get('runtime') or 0),
|
||||
'year': int(result.get('year') or 0),
|
||||
|
@ -275,11 +256,15 @@ class TorrentPlugin(Plugin):
|
|||
for quality, item in (episode.get('torrents', {}) or {}).items()
|
||||
if quality != '0'
|
||||
],
|
||||
key=lambda item: '{series}.{quality}.{season:02d}.{episode:02d}'.format(
|
||||
series=item.get('series'),
|
||||
quality=item.get('quality'),
|
||||
season=item.get('season'),
|
||||
episode=item.get('episode'),
|
||||
key=lambda item: (
|
||||
'.'.join(
|
||||
[
|
||||
item.get('series', ''),
|
||||
item.get('quality', ''),
|
||||
str(item.get('season', 0)).zfill(2),
|
||||
str(item.get('episode', 0)).zfill(2),
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -299,7 +284,6 @@ class TorrentPlugin(Plugin):
|
|||
info = {}
|
||||
file_info = {}
|
||||
|
||||
# noinspection HttpUrlsUsage
|
||||
if torrent.startswith('magnet:?'):
|
||||
magnet = torrent
|
||||
magnet_info = lt.parse_magnet_uri(magnet)
|
||||
|
@ -320,7 +304,9 @@ class TorrentPlugin(Plugin):
|
|||
'save_path': download_dir,
|
||||
}
|
||||
elif torrent.startswith('http://') or torrent.startswith('https://'):
|
||||
response = requests.get(torrent, allow_redirects=True)
|
||||
response = requests.get(
|
||||
torrent, timeout=self._http_timeout, allow_redirects=True
|
||||
)
|
||||
torrent_file = os.path.join(download_dir, self._generate_rand_filename())
|
||||
|
||||
with open(torrent_file, 'wb') as f:
|
||||
|
@ -328,13 +314,10 @@ class TorrentPlugin(Plugin):
|
|||
else:
|
||||
torrent_file = os.path.abspath(os.path.expanduser(torrent))
|
||||
if not os.path.isfile(torrent_file):
|
||||
raise RuntimeError(
|
||||
'{} is not a valid torrent file'.format(torrent_file)
|
||||
)
|
||||
raise RuntimeError(f'{torrent_file} is not a valid torrent file')
|
||||
|
||||
if torrent_file:
|
||||
file_info = lt.torrent_info(torrent_file)
|
||||
# noinspection PyArgumentList
|
||||
info = {
|
||||
'name': file_info.name(),
|
||||
'url': torrent,
|
||||
|
@ -352,7 +335,7 @@ class TorrentPlugin(Plugin):
|
|||
if event_hndl:
|
||||
event_hndl(event)
|
||||
except Exception as e:
|
||||
self.logger.warning('Exception in torrent event handler: {}'.format(str(e)))
|
||||
self.logger.warning('Exception in torrent event handler: %s', e)
|
||||
self.logger.exception(e)
|
||||
|
||||
def _torrent_monitor(self, torrent, transfer, download_dir, event_hndl, is_media):
|
||||
|
@ -364,9 +347,7 @@ class TorrentPlugin(Plugin):
|
|||
|
||||
while not transfer.is_finished():
|
||||
if torrent not in self.transfers:
|
||||
self.logger.info(
|
||||
'Torrent {} has been stopped and removed'.format(torrent)
|
||||
)
|
||||
self.logger.info('Torrent %s has been stopped and removed', torrent)
|
||||
self._fire_event(TorrentDownloadStopEvent(url=torrent), event_hndl)
|
||||
break
|
||||
|
||||
|
@ -383,7 +364,6 @@ class TorrentPlugin(Plugin):
|
|||
if is_media:
|
||||
from platypush.plugins.media import MediaPlugin
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
files = [f for f in files if MediaPlugin.is_video_file(f)]
|
||||
|
||||
self.torrent_state[torrent]['download_rate'] = status.download_rate
|
||||
|
@ -458,7 +438,6 @@ class TorrentPlugin(Plugin):
|
|||
|
||||
import libtorrent as lt
|
||||
|
||||
# noinspection PyArgumentList
|
||||
self._lt_session = lt.session()
|
||||
return self._lt_session
|
||||
|
||||
|
@ -512,9 +491,7 @@ class TorrentPlugin(Plugin):
|
|||
)
|
||||
|
||||
if torrent in self._sessions:
|
||||
self.logger.info(
|
||||
'A torrent session is already running for {}'.format(torrent)
|
||||
)
|
||||
self.logger.info('A torrent session is already running for %s', torrent)
|
||||
return self.torrent_state.get(torrent, {})
|
||||
|
||||
session = self._get_session()
|
||||
|
@ -543,9 +520,7 @@ class TorrentPlugin(Plugin):
|
|||
|
||||
self._fire_event(TorrentQueuedEvent(url=torrent), event_hndl)
|
||||
self.logger.info(
|
||||
'Downloading "{}" to "{}" from [{}]'.format(
|
||||
info['name'], download_dir, torrent
|
||||
)
|
||||
'Downloading "%s" to "%s" from [%s]', info['name'], download_dir, torrent
|
||||
)
|
||||
monitor_thread = self._torrent_monitor(
|
||||
torrent=torrent,
|
||||
|
@ -587,7 +562,7 @@ class TorrentPlugin(Plugin):
|
|||
"""
|
||||
|
||||
if torrent not in self.transfers:
|
||||
return None, "No transfer in progress for {}".format(torrent)
|
||||
return None, f"No transfer in progress for {torrent}"
|
||||
|
||||
if self.torrent_state[torrent].get('paused', False):
|
||||
self.transfers[torrent].resume()
|
||||
|
@ -605,9 +580,7 @@ class TorrentPlugin(Plugin):
|
|||
:type torrent: str
|
||||
"""
|
||||
|
||||
if torrent not in self.transfers:
|
||||
return None, "No transfer in progress for {}".format(torrent)
|
||||
|
||||
assert torrent in self.transfers, f"No transfer in progress for {torrent}"
|
||||
self.transfers[torrent].resume()
|
||||
|
||||
@action
|
||||
|
@ -619,13 +592,11 @@ class TorrentPlugin(Plugin):
|
|||
:type torrent: str
|
||||
"""
|
||||
|
||||
if torrent not in self.transfers:
|
||||
return None, "No transfer in progress for {}".format(torrent)
|
||||
assert torrent in self.transfers, f"No transfer in progress for {torrent}"
|
||||
|
||||
self.transfers[torrent].pause()
|
||||
del self.torrent_state[torrent]
|
||||
del self.transfers[torrent]
|
||||
|
||||
if torrent in self._sessions:
|
||||
del self._sessions[torrent]
|
||||
|
||||
|
@ -645,5 +616,10 @@ class TorrentPlugin(Plugin):
|
|||
name += hex(random.randint(0, 15))[2:].upper()
|
||||
return name + '.torrent'
|
||||
|
||||
categories = {
|
||||
'movies': search_movies,
|
||||
'tv': search_tv,
|
||||
}
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
Loading…
Reference in a new issue