2023-01-12 01:04:47 +00:00
|
|
|
#
|
|
|
|
# This file is part of SickGear.
|
|
|
|
#
|
|
|
|
# SickGear is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# SickGear is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
|
|
|
|
from lxml_etree import etree
|
|
|
|
|
|
|
|
from exceptions_helper import ex
|
|
|
|
|
|
|
|
import sickgear
|
|
|
|
from . import classes, helpers, logger
|
|
|
|
from .common import Quality
|
|
|
|
from .name_parser.parser import InvalidNameException, InvalidShowException, NameParser
|
|
|
|
|
|
|
|
from _23 import decode_str
|
|
|
|
from six import string_types
|
|
|
|
|
|
|
|
# noinspection PyUnreachableCode
|
|
|
|
if False:
|
|
|
|
# noinspection PyUnresolvedReferences
|
|
|
|
from typing import Any, AnyStr, Dict, List, Tuple
|
|
|
|
|
|
|
|
SUBJECT_FN_MATCHER = re.compile(r'"([^"]*)"')
|
|
|
|
RE_NORMAL_NAME = re.compile(r'\.\w{1,5}$')
|
|
|
|
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
def _platform_encode(p):
|
2023-01-12 01:04:47 +00:00
|
|
|
""" Return Unicode name, if not already Unicode, decode with UTF-8 or latin1 """
|
|
|
|
try:
|
|
|
|
return decode_str(p)
|
|
|
|
except (BaseException, Exception):
|
|
|
|
return decode_str(p, sickgear.SYS_ENCODING, errors='replace').replace('?', '!')
|
|
|
|
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
def _name_extractor(subject):
|
2023-01-12 01:04:47 +00:00
|
|
|
""" Try to extract a file name from a subject line, return `subject` if in doubt """
|
|
|
|
result = subject
|
|
|
|
for name in re.findall(SUBJECT_FN_MATCHER, subject):
|
|
|
|
name = name.strip(' "')
|
|
|
|
if name and RE_NORMAL_NAME.search(name):
|
|
|
|
result = name
|
2023-02-13 21:00:11 +00:00
|
|
|
return _platform_encode(result)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
def _get_season_nzbs(name, url_data, season):
|
2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
:param name: name
|
|
|
|
:type name: AnyStr
|
|
|
|
:param url_data: url data
|
|
|
|
:type url_data: Any
|
|
|
|
:param season: season
|
|
|
|
:type season: int
|
|
|
|
:return:
|
|
|
|
:rtype: Tuple[Dict, AnyStr]
|
|
|
|
"""
|
|
|
|
try:
|
2023-02-13 21:00:11 +00:00
|
|
|
show_xml = etree.ElementTree(etree.XML(url_data))
|
2023-01-12 01:04:47 +00:00
|
|
|
except SyntaxError:
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.error(f'Unable to parse the XML of {name}, not splitting it')
|
2023-01-12 01:04:47 +00:00
|
|
|
return {}, ''
|
|
|
|
|
|
|
|
filename = name.replace('.nzb', '')
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
nzb_element = show_xml.getroot()
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
regex = r'([\w\._\ ]+)[\._ ]S%02d[\._ ]([\w\._\-\ ]+)' % season
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
scene_name_match = re.search(regex, filename, re.I)
|
|
|
|
if scene_name_match:
|
|
|
|
show_name, quality_section = scene_name_match.groups()
|
2023-01-12 01:04:47 +00:00
|
|
|
else:
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.error('%s - Not a valid season pack scene name. If it\'s a valid one, log a bug.' % name)
|
2023-01-12 01:04:47 +00:00
|
|
|
return {}, ''
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
regex = r'(%s[\._]S%02d(?:[E0-9]+)\.[\w\._]+)' % (re.escape(show_name), season)
|
2023-01-12 01:04:47 +00:00
|
|
|
regex = regex.replace(' ', '.')
|
|
|
|
|
|
|
|
ep_files = {}
|
|
|
|
xmlns = None
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
for cur_file in list(nzb_element):
|
2023-01-12 01:04:47 +00:00
|
|
|
if not isinstance(cur_file.tag, string_types):
|
|
|
|
continue
|
|
|
|
xmlns_match = re.match(r'[{](https?://[A-Za-z0-9_./]+/nzb)[}]file', cur_file.tag)
|
|
|
|
if not xmlns_match:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
xmlns = xmlns_match.group(1)
|
|
|
|
match = re.search(regex, cur_file.get("subject"), re.I)
|
|
|
|
if not match:
|
|
|
|
# print curFile.get("subject"), "doesn't match", regex
|
|
|
|
continue
|
|
|
|
cur_ep = match.group(1)
|
2023-02-13 21:00:11 +00:00
|
|
|
fn = _name_extractor(cur_file.get('subject', ''))
|
2023-01-12 01:04:47 +00:00
|
|
|
if cur_ep == re.sub(r'\+\d+\.par2$', '', fn, flags=re.I):
|
2023-02-10 14:15:50 +00:00
|
|
|
bn, ext = os.path.splitext(fn)
|
2023-01-12 01:04:47 +00:00
|
|
|
cur_ep = re.sub(r'\.(part\d+|vol\d+(\+\d+)?)$', '', bn, flags=re.I)
|
2023-02-10 14:15:50 +00:00
|
|
|
bn, ext = os.path.splitext(cur_ep)
|
2023-01-12 01:04:47 +00:00
|
|
|
if isinstance(ext, string_types) \
|
|
|
|
and re.search(r'^\.(nzb|r\d{2}|rar|7z|zip|par2|vol\d+|nfo|srt|txt|bat|sh|mkv|mp4|avi|wmv)$', ext,
|
|
|
|
flags=re.I):
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.warning('Unable to split %s into episode nzb\'s' % name)
|
2023-01-12 01:04:47 +00:00
|
|
|
return {}, ''
|
|
|
|
if cur_ep not in ep_files:
|
|
|
|
ep_files[cur_ep] = [cur_file]
|
|
|
|
else:
|
|
|
|
ep_files[cur_ep].append(cur_file)
|
|
|
|
|
|
|
|
return ep_files, xmlns
|
|
|
|
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
def _create_nzb_string(file_elements, xmlns):
|
2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
:param file_elements: first element
|
|
|
|
:param xmlns: xmlns
|
|
|
|
:return:
|
|
|
|
:rtype: AnyStr
|
|
|
|
"""
|
2023-02-13 21:00:11 +00:00
|
|
|
root_element = etree.Element("nzb")
|
2023-01-12 01:04:47 +00:00
|
|
|
if xmlns:
|
2023-02-13 21:00:11 +00:00
|
|
|
root_element.set("xmlns", xmlns)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
for curFile in file_elements:
|
2023-02-13 21:00:11 +00:00
|
|
|
root_element.append(_strip_ns(curFile, xmlns))
|
2023-01-12 01:04:47 +00:00
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
return etree.tostring(root_element, encoding='utf-8')
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
def _save_nzb(nzb_name, nzb_string):
|
2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
:param nzb_name: nzb name
|
|
|
|
:type nzb_name: AnyStr
|
|
|
|
:param nzb_string: nzb string
|
|
|
|
:type nzb_string: AnyStr
|
|
|
|
"""
|
|
|
|
try:
|
2023-02-10 14:15:50 +00:00
|
|
|
with open(nzb_name + '.nzb', 'w') as nzb_fh:
|
2023-01-12 01:04:47 +00:00
|
|
|
nzb_fh.write(nzb_string)
|
|
|
|
|
|
|
|
except EnvironmentError as e:
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.error(f'Unable to save NZB: {ex(e)}')
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
def _strip_ns(element, ns):
|
2023-01-12 01:04:47 +00:00
|
|
|
element.tag = element.tag.replace("{" + ns + "}", "")
|
|
|
|
for curChild in list(element):
|
2023-02-13 21:00:11 +00:00
|
|
|
_strip_ns(curChild, ns)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
return element
|
|
|
|
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
def split_result(result):
|
2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
:param result: search result
|
|
|
|
:type result: sickgear.classes.SearchResult
|
|
|
|
:return: list of search results
|
|
|
|
:rtype: List[sickgear.classes.SearchResult]
|
|
|
|
"""
|
2024-11-26 18:10:23 +00:00
|
|
|
resp = helpers.get_url(result.url, failure_monitor=False, as_binary=True)
|
2023-01-12 01:04:47 +00:00
|
|
|
if None is resp:
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.error(f'Unable to load url {result.url}, can\'t download season NZB')
|
2023-01-12 01:04:47 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
# parse the season ep name
|
|
|
|
try:
|
|
|
|
np = NameParser(False, show_obj=result.show_obj)
|
|
|
|
parse_result = np.parse(result.name)
|
|
|
|
except InvalidNameException:
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.debug(f'Unable to parse the filename {result.name} into a valid episode')
|
2023-01-12 01:04:47 +00:00
|
|
|
return False
|
|
|
|
except InvalidShowException:
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.debug(f'Unable to parse the filename {result.name} into a valid show')
|
2023-01-12 01:04:47 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
# bust it up
|
|
|
|
season = parse_result.season_number if None is not parse_result.season_number else 1
|
|
|
|
|
2023-02-13 21:00:11 +00:00
|
|
|
separate_nzbs, xmlns = _get_season_nzbs(result.name, resp, season)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
result_list = []
|
|
|
|
|
|
|
|
for new_nzb in separate_nzbs:
|
|
|
|
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.debug(f'Split out {new_nzb} from {result.name}')
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
# parse the name
|
|
|
|
try:
|
|
|
|
np = NameParser(False, show_obj=result.show_obj)
|
|
|
|
parse_result = np.parse(new_nzb)
|
|
|
|
except InvalidNameException:
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.debug(f'Unable to parse the filename {new_nzb} into a valid episode')
|
2023-01-12 01:04:47 +00:00
|
|
|
return False
|
|
|
|
except InvalidShowException:
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.debug(f'Unable to parse the filename {new_nzb} into a valid show')
|
2023-01-12 01:04:47 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
# make sure the result is sane
|
|
|
|
if (None is not parse_result.season_number and season != parse_result.season_number) \
|
|
|
|
or (None is parse_result.season_number and 1 != season):
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.warning(f'Found {new_nzb} inside {result.name} but it doesn\'t seem to belong to the same season,'
|
|
|
|
f' ignoring it')
|
2023-01-12 01:04:47 +00:00
|
|
|
continue
|
|
|
|
elif 0 == len(parse_result.episode_numbers):
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.warning(f'Found {new_nzb} inside {result.name} but it doesn\'t seem to be a valid episode NZB,'
|
|
|
|
f' ignoring it')
|
2023-01-12 01:04:47 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
want_ep = True
|
|
|
|
for ep_no in parse_result.episode_numbers:
|
|
|
|
if not result.show_obj.want_episode(season, ep_no, result.quality):
|
2023-03-08 13:44:20 +00:00
|
|
|
logger.debug(f'Ignoring result {new_nzb} because we don\'t want an episode that is'
|
|
|
|
f' {Quality.qualityStrings[result.quality]}')
|
2023-01-12 01:04:47 +00:00
|
|
|
want_ep = False
|
|
|
|
break
|
|
|
|
if not want_ep:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# get all the associated episode objects
|
|
|
|
ep_obj_list = [] # type: List[sickgear.tv.TVEpisode]
|
|
|
|
for cur_ep in parse_result.episode_numbers:
|
|
|
|
ep_obj_list.append(result.show_obj.get_episode(season, cur_ep))
|
|
|
|
|
|
|
|
# make a result
|
|
|
|
nzb_result = classes.NZBDataSearchResult(ep_obj_list)
|
|
|
|
nzb_result.name = new_nzb
|
|
|
|
nzb_result.provider = result.provider
|
|
|
|
nzb_result.quality = result.quality
|
|
|
|
nzb_result.show_obj = result.show_obj
|
2023-02-13 21:00:11 +00:00
|
|
|
nzb_result.extraInfo = [_create_nzb_string(separate_nzbs[new_nzb], xmlns)]
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
result_list.append(nzb_result)
|
|
|
|
|
|
|
|
return result_list
|