Skip to content

Commit

Permalink
Merge pull request #7480 from pymedusa/release/release-0.3.9
Browse files Browse the repository at this point in the history
Release 0.3.9
  • Loading branch information
medariox authored Dec 12, 2019
2 parents 8b7f433 + 3bb8dba commit d0c136d
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 90 deletions.
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 0.3.9 (2019-12-12)

#### Improvements
- Improved qBittorrent client ([#7474](https://github.com/pymedusa/Medusa/pull/7474))

#### Fixes
- Fixed season pack downloads occurring even if not needed ([#7472](https://github.com/pymedusa/Medusa/pull/7472))
- Fixed changing default indexer language and initial indexer in config-general ([#7478](https://github.com/pymedusa/Medusa/pull/7478))

-----

## 0.3.8 (2019-12-08)

#### Improvements
Expand All @@ -6,7 +17,6 @@
- Improve a number of anime release names parsed by guessit ([#7418](https://github.com/pymedusa/Medusa/pull/7418)) ([#7396](https://github.com/pymedusa/Medusa/pull/7396)) ([#7427](https://github.com/pymedusa/Medusa/pull/7427))

#### Fixes

- Show Header: Fix showing correct amount of stars for the IMDB rating ([#7401](https://github.com/pymedusa/Medusa/pull/7401))
- Re-implement tvdb season poster/banners (was disabled because of tvdb api issues) ([#7460](https://github.com/pymedusa/Medusa/pull/7460))
- Fix showing the data directory in the bottom of some config pages ([#7424](https://github.com/pymedusa/Medusa/pull/7424))
Expand Down Expand Up @@ -60,7 +70,7 @@
- Converted the footer to a Vue component ([#4520](https://github.com/pymedusa/Medusa/pull/4520))
- Converted Edit Show to a Vue SFC ([#4486](https://github.com/pymedusa/Medusa/pull/4486)
- Improved API v2 exception reporting on Python 2 ([#6931](https://github.com/pymedusa/Medusa/pull/6931))
- Added support for qbittorrent api v2. Required from qbittorrent version > 3.2.0. ([#7040](https://github.com/pymedusa/Medusa/pull/7040))
- Added support for qBittorrent API v2. Required from qBittorrent version 4.2.0. ([#7040](https://github.com/pymedusa/Medusa/pull/7040))
- Removed the forced search queue item in favor of the backlog search queue item. ([#6718](https://github.com/pymedusa/Medusa/pull/6718))
- Show Header: Improved visibility of local and global configured required and ignored words. ([#7085](https://github.com/pymedusa/Medusa/pull/7085))
- Reduced frequency of file system access when not strictly required ([#7102](https://github.com/pymedusa/Medusa/pull/7102))
Expand Down
12 changes: 6 additions & 6 deletions medusa/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,14 @@ def _create_episode_objects(self):
if self.actual_season is not None and self.series:
if self.actual_episodes:
self.episodes = [self.series.get_episode(self.actual_season, ep) for ep in self.actual_episodes]
if len(self.actual_episodes) == 1:
self.episode_number = self.actual_episodes[0]
else:
self.episode_number = MULTI_EP_RESULT
else:
self.episodes = self.series.get_all_episodes(self.actual_season)
self.actual_episodes = [ep.episode for ep in self.episodes]
self.episode_number = SEASON_RESULT

return self.episodes

Expand All @@ -245,12 +251,6 @@ def _episodes_from_cache(self):
# Season result
if not sql_episodes:
ep_objs = series_obj.get_all_episodes(actual_season)
if not ep_objs:
# We couldn't get any episodes for this season, which is odd, skip the result.
log.debug("We couldn't get any episodes for season {0} of {1}, skipping",
actual_season, cached_data['name'])
return

self.actual_episodes = [ep.episode for ep in ep_objs]
self.episode_number = SEASON_RESULT

Expand Down
143 changes: 86 additions & 57 deletions medusa/clients/torrent/qbittorrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@
from medusa.logger.adapters.style import BraceAdapter

from requests.auth import HTTPDigestAuth
from requests.compat import urljoin

log = BraceAdapter(logging.getLogger(__name__))
log.logger.addHandler(logging.NullHandler())


class APIUnavailableError(Exception):
"""Raised when the API version is not available."""


class QBittorrentAPI(GenericClient):
"""qBittorrent API class."""

Expand All @@ -29,47 +34,54 @@ def __init__(self, host=None, username=None, password=None):
:param password:
:type password: string
"""
super(QBittorrentAPI, self).__init__('qbittorrent', host, username, password)
super(QBittorrentAPI, self).__init__('qBittorrent', host, username, password)
self.url = self.host
# Auth for API v1.0.0 (qBittorrent v3.1.x and older)
self.session.auth = HTTPDigestAuth(self.username, self.password)

@property
def api(self):
"""Get API version."""
# Update the auth method to v2
self._get_auth = self._get_auth_v2
# Attempt to get API v2 version first
self.url = '{host}api/v2/app/webapiVersion'.format(host=self.host)
try:
version = self.session.get(self.url, verify=app.TORRENT_VERIFY_CERT,
cookies=self.session.cookies)
# Make sure version is using the (major, minor, release) format
version = list(map(int, version.text.split('.')))
if len(version) < 2:
version.append(0)
return tuple(version)
except (AttributeError, ValueError) as error:
log.error('{name}: Unable to get API version. Error: {error!r}',
{'name': self.name, 'error': error})

# Fall back to API v1
self._get_auth = self._get_auth_legacy
try:
self.url = '{host}version/api'.format(host=self.host)
version = int(self.session.get(self.url, verify=app.TORRENT_VERIFY_CERT).content)
# Convert old API versioning to new versioning (major, minor, release)
version = (1, version % 100, 0)
except Exception:
version = (1, 0, 0)
return version
self.api = None

def _get_auth(self):
"""Select between api v2 and legacy."""
return self._get_auth_v2() or self._get_auth_legacy()
"""Authenticate with the client using the most recent API version available for use."""
try:
auth = self._get_auth_v2()
version = 2
except APIUnavailableError:
auth = self._get_auth_legacy()
version = 1

# Authentication failed /or/ We already have the API version
if not auth or self.api:
return auth

# Get API version
if version == 2:
self.url = urljoin(self.host, 'api/v2/app/webapiVersion')
try:
response = self.session.get(self.url, verify=app.TORRENT_VERIFY_CERT)
if not response.text:
raise ValueError('Response from client is empty. [Status: {0}]'.format(response.status_code))
# Make sure version is using the (major, minor, release) format
version = tuple(map(int, response.text.split('.')))
# Fill up with zeros to get the correct format. e.g: (2, 3) => (2, 3, 0)
self.api = version + (0,) * (3 - len(version))
except (AttributeError, ValueError) as error:
log.error('{name}: Unable to get API version. Error: {error!r}',
{'name': self.name, 'error': error})
elif version == 1:
try:
self.url = urljoin(self.host, 'version/api')
version = int(self.session.get(self.url, verify=app.TORRENT_VERIFY_CERT).text)
# Convert old API versioning to new versioning (major, minor, release)
self.api = (1, version % 100, 0)
except Exception:
self.api = (1, 0, 0)

return auth

def _get_auth_v2(self):
"""Authenticate using the new method (API v2)."""
self.url = '{host}api/v2/auth/login'.format(host=self.host)
"""Authenticate using API v2."""
self.url = urljoin(self.host, 'api/v2/auth/login')
data = {
'username': self.username,
'password': self.password,
Expand All @@ -79,17 +91,31 @@ def _get_auth_v2(self):
except Exception:
return None

if self.response.status_code == 404:
return None
if self.response.status_code == 200:
if self.response.text == 'Fails.':
log.warning('{name}: Invalid Username or Password, check your config',
{'name': self.name})
return None

self.session.cookies = self.response.cookies
self.auth = self.response.content
# Successful log in
self.session.cookies = self.response.cookies
self.auth = self.response.text

return self.auth
return self.auth

if self.response.status_code == 404:
# API v2 is not available
raise APIUnavailableError()

if self.response.status_code == 403:
log.warning('{name}: Your IP address has been banned after too many failed authentication attempts.'
' Restart {name} to unban.',
{'name': self.name})
return None

def _get_auth_legacy(self):
"""Authenticate using the legacy method (API v1)."""
self.url = '{host}login'.format(host=self.host)
"""Authenticate using legacy API."""
self.url = urljoin(self.host, 'login')
data = {
'username': self.username,
'password': self.password,
Expand All @@ -99,22 +125,22 @@ def _get_auth_legacy(self):
except Exception:
return None

# Pre-API v1
# API v1.0.0 (qBittorrent v3.1.x and older)
if self.response.status_code == 404:
try:
self.response = self.session.get(self.host, verify=app.TORRENT_VERIFY_CERT)
except Exception:
return None

self.session.cookies = self.response.cookies
self.auth = self.response.content
self.auth = (self.response.status_code != 404) or None

return self.auth if not self.response.status_code == 404 else None
return self.auth

def _add_torrent_uri(self, result):

command = 'api/v2/torrents/add' if self.api >= (2, 0, 0) else 'command/download'
self.url = '{host}{command}'.format(host=self.host, command=command)
self.url = urljoin(self.host, command)
data = {
'urls': result.url,
}
Expand All @@ -123,7 +149,7 @@ def _add_torrent_uri(self, result):
def _add_torrent_file(self, result):

command = 'api/v2/torrents/add' if self.api >= (2, 0, 0) else 'command/upload'
self.url = '{host}{command}'.format(host=self.host, command=command)
self.url = urljoin(self.host, command)
files = {
'torrents': (
'{result}.torrent'.format(result=result.name),
Expand All @@ -140,27 +166,30 @@ def _set_torrent_label(self, result):

api = self.api
if api >= (2, 0, 0):
self.url = '{host}api/v2/torrents/setCategory'.format(host=self.host)
self.url = urljoin(self.host, 'api/v2/torrents/setCategory')
label_key = 'category'
elif api > (1, 6, 0):
label_key = 'Category' if api >= (1, 10, 0) else 'Label'
self.url = '{host}command/set{key}'.format(
host=self.host,
key=label_key,
)
self.url = urljoin(self.host, 'command/set' + label_key)

data = {
'hashes': result.hash.lower(),
label_key.lower(): label.replace(' ', '_'),
}
return self._request(method='post', data=data, cookies=self.session.cookies)
ok = self._request(method='post', data=data, cookies=self.session.cookies)

if self.response.status_code == 409:
log.warning('{name}: Unable to set torrent label. You need to create the label '
' in {name} first.', {'name': self.name})
ok = False

return ok

def _set_torrent_priority(self, result):

command = 'api/v2/torrents' if self.api >= (2, 0, 0) else 'command'
method = 'increase' if result.priority == 1 else 'decrease'
self.url = '{host}{command}/{method}Prio'.format(
host=self.host, command=command, method=method)
self.url = urljoin(self.host, '{command}/{method}Prio'.format(command=command, method=method))
data = {
'hashes': result.hash.lower(),
}
Expand All @@ -178,7 +207,7 @@ def _set_torrent_pause(self, result):
state = 'pause' if app.TORRENT_PAUSED else 'resume'
command = 'api/v2/torrents' if api >= (2, 0, 0) else 'command'
hashes_key = 'hashes' if self.api >= (1, 18, 0) else 'hash'
self.url = '{host}{command}/{state}'.format(host=self.host, command=command, state=state)
self.url = urljoin(self.host, '{command}/{state}'.format(command=command, state=state))
data = {
hashes_key: result.hash.lower(),
}
Expand All @@ -196,10 +225,10 @@ def remove_torrent(self, info_hash):
'hashes': info_hash.lower(),
}
if self.api >= (2, 0, 0):
self.url = '{host}api/v2/torrents/delete'.format(host=self.host)
self.url = urljoin(self.host, 'api/v2/torrents/delete')
data['deleteFiles'] = True
else:
self.url = '{host}command/deletePerm'.format(host=self.host)
self.url = urljoin(self.host, 'command/deletePerm')

return self._request(method='post', data=data, cookies=self.session.cookies)

Expand Down
2 changes: 1 addition & 1 deletion medusa/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
log.logger.addHandler(logging.NullHandler())

INSTANCE_ID = text_type(uuid.uuid1())
VERSION = '0.3.8'
VERSION = '0.3.9'
USER_AGENT = 'Medusa/{version} ({system}; {release}; {instance})'.format(
version=VERSION, system=platform.system(), release=platform.release(),
instance=INSTANCE_ID)
Expand Down
17 changes: 7 additions & 10 deletions medusa/providers/generic_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,23 +424,20 @@ def find_search_results(self, series, episodes, search_mode, forced_search=False

search_result.update_search_result()

if not search_result.actual_episodes:
episode_number = SEASON_RESULT
if search_result.episode_number == SEASON_RESULT:
log.debug('Found season pack result {0} at {1}', search_result.name, search_result.url)
elif len(search_result.actual_episodes) == 1:
episode_number = search_result.actual_episode
log.debug('Found single episode result {0} at {1}', search_result.name, search_result.url)
else:
episode_number = MULTI_EP_RESULT
elif search_result.episode_number == MULTI_EP_RESULT:
log.debug('Found multi-episode ({0}) result {1} at {2}',
', '.join(map(str, search_result.parsed_result.episode_numbers)),
search_result.name,
search_result.url)
else:
log.debug('Found single episode result {0} at {1}', search_result.name, search_result.url)

if episode_number not in final_results:
final_results[episode_number] = [search_result]
if search_result.episode_number not in final_results:
final_results[search_result.episode_number] = [search_result]
else:
final_results[episode_number].append(search_result)
final_results[search_result.episode_number].append(search_result)

if cl:
# Access to a protected member of a client class
Expand Down
9 changes: 4 additions & 5 deletions medusa/tv/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,11 +609,10 @@ def find_episodes(self, episodes):
continue

search_result = self.provider.get_result(series=series_obj, cache=cur_result)
if search_result.episode_number is not None:
if search_result in cache_results[search_result.episode_number]:
continue
# add it to the list
cache_results[search_result.episode_number].append(search_result)
if search_result in cache_results[search_result.episode_number]:
continue
# add it to the list
cache_results[search_result.episode_number].append(search_result)

# datetime stamp this search so cache gets cleared
self.searched = time()
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ filterwarnings =
ignore::PendingDeprecationWarning
flake8-ignore =
D107
D401
W503
W504
medusa/__init__.py D104 F401
Expand Down
17 changes: 12 additions & 5 deletions themes-default/slim/src/components/config-general.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@
<fieldset class="component-group-list">

<config-template label-for="show_root_dir" label="Default Indexer Language">
<language-select @update-language="indexerLanguage = $event" ref="indexerLanguage" :language="config.indexerDefaultLanguage" :available="indexers.main.validLanguages.join(',')" class="form-control form-control-inline input-sm" />
<language-select @update-language="config.indexerDefaultLanguage = $event" ref="indexerLanguage"
:language="config.indexerDefaultLanguage" :available="indexers.main.validLanguages.join(',')"
class="form-control form-control-inline input-sm" />
<span>for adding shows and metadata providers</span>
</config-template>

Expand Down Expand Up @@ -590,10 +592,15 @@ export default {
...mapGetters([
'getStatus'
]),
indexerDefault() {
const { config } = this;
const { indexerDefault } = config;
return indexerDefault || 0;
indexerDefault: {
get() {
const { config } = this;
const { indexerDefault } = config;
return indexerDefault || 0;
},
set(indexer) {
this.config.indexerDefault = indexer;
}
},
indexerListOptions() {
const { indexers } = this;
Expand Down
Loading

0 comments on commit d0c136d

Please sign in to comment.