# -*- coding: utf-8 -*- """ Download handler module Copyright (c) 2018, Leo Moll SPDX-License-Identifier: MIT """ # -- Imports ------------------------------------------------ from __future__ import unicode_literals import os import re from contextlib import closing # pylint: disable=import-error import xbmcvfs import resources.lib.mvutils as mvutils from resources.lib.kodi.kodiui import KodiProgressDialog from resources.lib.filmui import FilmUI from resources.lib.ttml2srt import ttml2srt class Downloader(object): """ This class handles downloads of films and subtitles. In addition it is able to handle subtitle conversion, NFO file creation and some special tasks reguarding play with subtitles. Args: plugin(MediathekView): the plugin object """ def __init__(self, plugin): self.plugin = plugin self.database = plugin.database self.settings = plugin.settings self.notifier = plugin.notifier self.plugin.datapath = self.plugin.datapath def play_movie_with_subs(self, filmid): """ Play the specified film with subtitles. Since the subtitles are not available in a playable format, they have to be downloaded and converted first. Args: filmid(id): database id of the film to play """ film = self.database.retrieve_film_info(filmid) if film is None: self.notifier.show_error(30990, self.plugin.language(30991)) return ttmname = os.path.join(self.plugin.datapath, 'subtitle.ttml') srtname = os.path.join(self.plugin.datapath, 'subtitle.srt') subs = [] if self.download_subtitle(film, ttmname, srtname, 'subtitle'): subs.append(srtname) (_, listitem) = FilmUI(self.plugin).get_list_item(None, film) if listitem: if subs: listitem.setSubtitles(subs) self.plugin.set_resolved_url(True, listitem) def download_subtitle(self, film, ttmname, srtname, filename): """ Downloads and converts a subtitle track of a film to SRT format. Args: film(Film): the film object loaded from the database ttmname(str): the filename of the downloaded subtitle in original format srtname(str): the filename of the downloaded subtitle file after conversion to SRT format filename(str): a filename stub without extension for UI display """ ret = False if film.url_sub: progress = KodiProgressDialog() progress.create(30978, filename + u'.ttml') # pylint: disable=broad-except try: progress.update(0) mvutils.url_retrieve_vfs( film.url_sub, ttmname, progress.url_retrieve_hook) try: ttml2srtConverter = ttml2srt() ttml2srtConverter.do(xbmcvfs.File(ttmname, 'r'), xbmcvfs.File(srtname, 'w')) ret = True except Exception as err: self.plugin.info('Failed to convert to srt: {}', err) progress.close() except Exception as err: progress.close() self.plugin.error( 'Failure downloading {}: {}', film.url_sub, err) return ret def download_movie(self, filmid, quality): """ Downloads a film as a movie in the movie download directory. This implies a specific naming scheme and the generation of a movie NFO file. Args: filmid(id): database id of the film to download quality(int): quality to download (0 = SD, 1 = Normal, 2 = HD) """ if not self._test_download_path(self.settings.downloadpathmv): return film = self.database.retrieve_film_info(filmid) if film is None: return (filmurl, suffix, extension, ) = self._get_film_url_and_extension(film, quality) # try to create a good name for the downloaded file namestem = mvutils.cleanup_filename(film.title)[:80] if not namestem: # try to take the show name instead... namestem = mvutils.cleanup_filename(film.show)[:64] if not namestem: namestem = u'Film' namestem = namestem + '-{}'.format(film.filmid) elif self.settings.movienamewithshow: showname = mvutils.cleanup_filename(film.show)[:64] if showname: namestem = showname + ' - ' + namestem # review name if self.settings.reviewname: (namestem, confirmed) = self.notifier.get_entered_text(namestem, 30986) namestem = mvutils.cleanup_filename(namestem) if len(namestem) < 1 or confirmed is False: return # build year postfix year = self._matches('([12][0-9][0-9][0-9])', str(film.aired)) if year is not None: postfix = ' (%s)' % year else: postfix = '' # determine destination path and film filename if self.settings.moviefolders: pathname = self.settings.downloadpathmv + namestem + postfix + '/' filename = namestem + suffix else: pathname = self.settings.downloadpathmv filename = namestem + postfix + suffix # check for duplicate while xbmcvfs.exists(pathname + filename + extension): (filename, confirmed) = self.notifier.get_entered_text(filename, 30987) filename = mvutils.cleanup_filename(filename) if len(filename) < 1 or confirmed is False: return # download the stuff if self._download_files(film, filmurl, pathname, filename, extension): self._make_movie_nfo_file(film, filmurl, pathname, filename) def download_episode(self, filmid, quality): """ Downloads a film as a series episode in the series download directory. This implies a specific naming scheme and the generation of a tvshow and episode NFO file. Args: filmid(id): database id of the film to download quality(int): quality to download (0 = SD, 1 = Normal, 2 = HD) """ if not self._test_download_path(self.settings.downloadpathep): return film = self.database.retrieve_film_info(filmid) if film is None: return (filmurl, suffix, extension, ) = self._get_film_url_and_extension(film, quality) # detect season and episode (season, episode, fninfo, ) = self._season_and_episode_detect(film) # determine names showname = mvutils.cleanup_filename(film.show)[:64] namestem = mvutils.cleanup_filename(film.title)[:80] if not namestem: namestem = u'Episode-{}'.format(film.filmid) if not showname: showname = namestem # review name if self.settings.reviewname: (namestem, confirmed) = self.notifier.get_entered_text(namestem, 30986) namestem = mvutils.cleanup_filename(namestem) if len(namestem) < 1 or confirmed is False: return # prepare download directory and determine sequence number pathname = self.settings.downloadpathep + showname + '/' sequence = 1 if xbmcvfs.exists(pathname): (_, epfiles, ) = xbmcvfs.listdir(pathname) for epfile in epfiles: match = re.search(r'^.* - \(([0-9]*)\)\.[^/]*$', epfile) if match and match.groups(): if sequence <= int(match.group(1)): sequence = int(match.group(1)) + 1 else: xbmcvfs.mkdir(pathname) filename = showname + ' - ' + fninfo + \ namestem + (u' - (%04d)' % sequence) + suffix # download the stuff if self._download_files(film, filmurl, pathname, filename, extension): self._make_series_nfo_files( film, filmurl, pathname, filename, season, episode, sequence) def _download_files(self, film, filmurl, pathname, filename, extension): # make sure the destination directory exists if not xbmcvfs.exists(pathname): xbmcvfs.mkdir(pathname) # prepare resulting filenames movname = pathname + filename + extension srtname = pathname + filename + u'.srt' ttmname = pathname + filename + u'.ttml' # download video progress = KodiProgressDialog() progress.create(self.plugin.language(30974), filename + extension) # pylint: disable=broad-except try: progress.update(0) mvutils.url_retrieve_vfs( filmurl, movname, progress.url_retrieve_hook) progress.close() self.notifier.show_notification( 30960, self.plugin.language(30976).format(filmurl)) except Exception as err: progress.close() self.plugin.error('Failure downloading {}: {}', filmurl, err) self.notifier.show_error( 30952, self.plugin.language(30975).format(filmurl, err)) return False # download subtitles if self.settings.downloadsrt and film.url_sub: self.download_subtitle(film, ttmname, srtname, filename) return True def _test_download_path(self, downloadpath): if not downloadpath: self.notifier.show_error(30952, 30958) return False # check if the download path is reachable if not xbmcvfs.exists(downloadpath): self.notifier.show_error(30952, 30979) return False return True @staticmethod def _get_film_url_and_extension(film, quality): # get the best url if quality == '0' and film.url_video_sd: suffix = '' filmurl = film.url_video_sd elif quality == '2' and film.url_video_hd: suffix = '.720p' filmurl = film.url_video_hd else: suffix = '' filmurl = film.url_video extension = os.path.splitext(filmurl)[1] if extension: return (filmurl, suffix, extension, ) else: return (filmurl, suffix, u'.mp4', ) def _make_movie_nfo_file(self, film, filmurl, pathname, filename): # create movie NFO file # See: https://kodi.wiki/view/NFO_files/Movies # pylint: disable=broad-except if self.settings.makenfo > 0: try: # bug of pylint 1.7.1 - See https://github.com/PyCQA/pylint/issues/1444 # pylint: disable=no-member with closing(xbmcvfs.File(pathname + filename + u'.nfo', 'w')) as nfofile: nfofile.write(bytearray('<movie>\n', 'utf-8')) nfofile.write( bytearray('\t<title>{}</title>\n'.format(film.title), 'utf-8')) nfofile.write( bytearray('\t<plot>{}</plot>\n'.format(film.description), 'utf-8')) nfofile.write( bytearray('\t<studio>{}</studio>\n'.format(film.channel), 'utf-8')) aired = self._matches( '([12][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9])', str(film.aired)) if aired is not None: nfofile.write( bytearray('\t<aired>{}</aired>\n'.format(aired), 'utf-8')) year = self._matches( '([12][0-9][0-9][0-9])', str(film.aired)) if year is not None: nfofile.write( bytearray('\t<year>{}</year>\n'.format(year), 'utf-8')) if film.seconds > 60: nfofile.write( bytearray( '\t<runtime>{}</runtime>\n'.format( int(film.seconds / 60) ), 'utf-8' ) ) nfofile.write(bytearray('</movie>\n', 'utf-8')) except Exception as err: self.plugin.error( 'Failure creating episode NFO file for {}: {}', filmurl, err) def _make_series_nfo_files(self, film, filmurl, pathname, filename, season, episode, sequence): # create series NFO files # See: https://kodi.wiki/view/NFO_files/TV_shows # pylint: disable=broad-except if self.settings.makenfo > 0: aired = self._matches( '([12][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9])', str(film.aired)) year = self._matches('([12][0-9][0-9][0-9])', str(film.aired)) if not xbmcvfs.exists(pathname + 'tvshow.nfo'): try: # bug of pylint 1.7.1 - See https://github.com/PyCQA/pylint/issues/1444 # pylint: disable=no-member with closing(xbmcvfs.File(pathname + 'tvshow.nfo', 'w')) as nfofile: nfofile.write(bytearray('<tvshow>\n', 'utf-8')) nfofile.write(bytearray('\t<id></id>\n', 'utf-8')) nfofile.write( bytearray('\t<title>{}</title>\n'.format(film.show), 'utf-8')) nfofile.write( bytearray('\t<sorttitle>{}</sorttitle>\n'.format(film.show), 'utf-8')) nfofile.write( bytearray('\t<studio>{}</studio>\n'.format(film.channel), 'utf-8')) if year is not None: nfofile.write( bytearray('\t<year>{}</year>\n'.format(year), 'utf-8')) nfofile.write(bytearray('</tvshow>\n', 'utf-8')) except Exception as err: self.plugin.error( 'Failure creating show NFO file for {}: {}', filmurl, err) try: # bug of pylint 1.7.1 - See https://github.com/PyCQA/pylint/issues/1444 # pylint: disable=no-member with closing(xbmcvfs.File(pathname + filename + u'.nfo', 'w')) as nfofile: nfofile.write(bytearray('<episodedetails>\n', 'utf-8')) nfofile.write( bytearray('\t<title>{}</title>\n'.format(film.title), 'utf-8')) if self.settings.makenfo == 2 and season is not None and episode is not None: nfofile.write( bytearray('\t<season>{}</season>\n'.format(season), 'utf-8')) nfofile.write( bytearray('\t<episode>{}</episode>\n'.format(episode), 'utf-8')) elif self.settings.makenfo == 2 and episode is not None: nfofile.write( bytearray('\t<season>1</season>\n', 'utf-8')) nfofile.write( bytearray('\t<episode>{}</episode>\n'.format(episode), 'utf-8')) elif self.settings.makenfo == 2: nfofile.write( bytearray('\t<season>999</season>\n', 'utf-8')) nfofile.write( bytearray('\t<episode>{}</episode>\n'.format(sequence), 'utf-8')) nfofile.write( bytearray('\t<autonumber>1</autonumber>\n', 'utf-8')) nfofile.write( bytearray('\t<showtitle>{}</showtitle>\n'.format(film.show), 'utf-8')) nfofile.write( bytearray('\t<plot>{}</plot>\n'.format(film.description), 'utf-8')) if aired is not None: nfofile.write( bytearray('\t<aired>{}</aired>\n'.format(aired), 'utf-8')) if year is not None: nfofile.write( bytearray('\t<year>{}</year>\n'.format(year), 'utf-8')) if film.seconds > 60: nfofile.write( bytearray( '\t<runtime>{}</runtime>\n'.format( int(film.seconds / 60) ), 'utf-8' ) ) nfofile.write( bytearray('\t<studio>{}</studio>\n'.format(film.channel), 'utf-8')) nfofile.write(bytearray('</episodedetails>\n', 'utf-8')) except Exception as err: self.plugin.error( 'Failure creating episode NFO file for {}: {}', filmurl, err) def _season_and_episode_detect(self, film): # initial trivial implementation self.plugin.error('film.show is type {}', type(film.show)) self.plugin.error('film.title is type {}', type(film.title)) season = self._matches(r'staffel[\.:\- ]+([0-9]+)', film.title) if season is None: season = self._matches(r'([0-9]+)[\.:\- ]+staffel', film.title) if season is None: season = self._matches(r'staffel[\.:\- ]+([0-9]+)', film.show) if season is None: season = self._matches(r'([0-9]+)[\.:\- ]+staffel', film.show) episode = self._matches(r'episode[\.:\- ]+([0-9]+)', film.title) if episode is None: episode = self._matches(r'folge[\.:\- ]+([0-9]+)', film.title) if episode is None: episode = self._matches(r'teil[\.:\- ]+([0-9]+)', film.title) if episode is None: episode = self._matches(r'([0-9]+)[\.:\- ]+teil', film.title) if episode is None: episode = self._matches(r'\(([0-9]+)\/[0-9]', film.title) # generate filename info if season is not None and episode is not None: return (season, episode, 'S%02dE%02d - ' % (int(season), int(episode)), ) elif episode is not None: return (None, episode, 'EP%03d - ' % int(episode)) else: return (None, None, '', ) @staticmethod def _matches(regex, test): if test is not None: match = re.search(regex, test, flags=re.IGNORECASE) if match and match.groups(): return match.group(1) return None