Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/show sub original release #7955

Merged
merged 19 commits into from
Apr 19, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#### Improvements
- Add show names with dashes to guessit expected titles ([#7918](https://github.com/pymedusa/Medusa/pull/7918))
- Provider YggTorrents: Add 'saison' as a season pack search keyword ([#7920](https://github.com/pymedusa/Medusa/pull/7920))
- Show Snatched or Downloaded release name when manually picking a subtitle ([#7955](https://github.com/pymedusa/Medusa/pull/7955))

#### Fixes
- Fixed root dirs not always shown on Home page ([#7921](https://github.com/pymedusa/Medusa/pull/7921))
Expand Down
113 changes: 113 additions & 0 deletions dredd/api-description.yml
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,66 @@ paths:
400:
$ref: '#/responses/error'
x-disabled: true
/history/{seriesid}:
get:
summary: Return history entries to a specific show
description: |
The history endpoint returns logged activities stored in the history table, like episodes snatched and downloaded. Or downloaded subtitles for an episode.
parameters:
- $ref: '#/parameters/series-id'
name: seriesid
- $ref: '#/parameters/page'
- $ref: '#/parameters/limit'
- $ref: '#/parameters/sort'
responses:
200:
$ref: '#/responses/pagination'
description: A paged array of history entries
schema:
type: array
items:
$ref: '#/definitions/History'
400:
$ref: '#/responses/error'
description: Invalid series id or pagination parameters
x-request:
query-params:
sort: abc
404:
$ref: '#/responses/error'
description: Series not found
x-request:
path-params:
seriesid: tvdb999999999
/history/{seriesid}/episode/{episodeid}:
get:
summary: Return history entries for a specific episode
description: |
The histories episode endpoint returns history entries for a specific episode
parameters:
- $ref: '#/parameters/series-id'
name: seriesid
- $ref: '#/parameters/episode-id'
name: episodeid
responses:
200:
description: Array of History entries
schema:
type: array
items:
$ref: '#/definitions/History'
400:
$ref: '#/responses/error'
description: Invalid series id or pagination parameters
x-request:
query-params:
sort: abc
404:
$ref: '#/responses/error'
description: Series not found
x-request:
path-params:
seriesid: tvdb999999999

definitions:
Series:
Expand Down Expand Up @@ -2721,6 +2781,59 @@ definitions:
overview:
type: string
description: Episode status/quality overview string

History:
type: object
properties:
id:
type: integer
format: int32
description: Internal id for the history row
series:
type: string
description: Series slug (if available)
status:
type: integer
format: int32
description: Status (numberic)
statusName:
type: string
description: Status description
actionDate:
type: integer
format: int32
description: Date of when the history entrie was stored
resource:
type: string
description: Description of what was stored
example:
- The release name for a statusName of "Downloaded" or "Snatched"
- The language of a subitle downloaded fo ra statusName of "Subtitled"
size:
type: integer
description: Snatched or Downloaded filesize
season:
type: integer
description: Season number
episode:
type: integer
description: Episode number
manuallySearched:
type: boolean
description: Specifies if an episode was snatched or downloaded through a manual search
provider:
type: object
properties:
id:
type: string
description: Provider id
name:
type: string
description: Provider name
imagename:
type: string
description: Provider icon image name.

parameters:
detailed:
name: detailed
Expand Down
140 changes: 140 additions & 0 deletions medusa/server/api/v2/episode_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# coding=utf-8
"""Request handler for series and episodes."""
from __future__ import unicode_literals

import logging

from medusa import db

from medusa.common import statusStrings
from medusa.helper.exceptions import EpisodeDeletedException
from medusa.logger.adapters.style import BraceAdapter
from medusa.providers.generic_provider import GenericProvider
from medusa.providers import get_provider_class
from medusa.server.api.v2.base import BaseRequestHandler
from medusa.server.api.v2.history import HistoryHandler
from medusa.tv.episode import Episode, EpisodeNumber
from medusa.tv.series import Series, SeriesIdentifier

from os.path import basename

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


class EpisodeHistoryHandler(BaseRequestHandler):
"""Episode history request handler."""

#: parent resource handler
parent_handler = HistoryHandler
#: resource name
name = 'episode'
#: identifier
identifier = ('episode_slug', r'[\w-]+')
#: path param
path_param = ('path_param', r'\w+')
#: allowed HTTP methods
allowed_methods = ('GET', 'DELETE',)

def get(self, series_slug, episode_slug, path_param):
"""Query episode's history information.

:param series_slug: series slug. E.g.: tvdb1234
:param episode_slug: episode slug. E.g.: s01e01
:param path_param:
"""
series_identifier = SeriesIdentifier.from_slug(series_slug)
if not series_identifier:
return self._bad_request('Invalid series slug')

series = Series.find_by_identifier(series_identifier)
if not series:
return self._not_found('Series not found')

if not episode_slug:
return self._bad_request('Invalid episode slug')

episode_number = EpisodeNumber.from_slug(episode_slug)
if not episode_number:
return self._not_found('Invalid episode number')

episode = Episode.find_by_series_and_episode(series, episode_number)
if not episode:
return self._not_found('Episode not found')

sql_base = '''
SELECT rowid, date, action, quality,
provider, version, resource, size, proper_tags,
indexer_id, showid, season, episode, manually_searched
FROM history
WHERE showid = ? AND indexer_id = ? AND season = ? AND episode = ?
'''

params = [series.series_id, series.indexer, episode.season, episode.episode]

sql_base += ' ORDER BY date DESC'
results = db.DBConnection().select(sql_base, params)

def data_generator():
"""Read history data and normalize key/value pairs."""
for item in results:
d = {}
d['id'] = item['rowid']

if item['indexer_id'] and item['showid']:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these really be missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For an episode it can't.
But i've been overthinking the decistion to create the episode_history.py route. (apiv2/history/tvdb12345/episode/s01e01). But maybe I should just use a parameter to specify an episode?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's consistent with the rest of the routes. But since they can't be missing for episode, we should raise an exception or leave the check out completely?

d['series'] = SeriesIdentifier.from_id(item['indexer_id'], item['showid']).slug

d['status'] = item['action']
d['actionDate'] = item['date']

d['resource'] = basename(item['resource'])
d['size'] = item['size']
d['properTags'] = item['proper_tags']
d['statusName'] = statusStrings.get(item['action'])
d['season'] = item['season']
d['episode'] = item['episode']
d['manuallySearched'] = bool(item['manually_searched'])

provider = get_provider_class(GenericProvider.make_id(item['provider']))
d['provider'] = {}
if provider:
d['provider']['id'] = provider.get_id()
d['provider']['name'] = provider.name
d['provider']['imageName'] = provider.image_name()

yield d

if not len(results):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think len() is needed here

return self._not_found('History data not found for show {show} and episode {episode}'.format(
show=series.identifier.slug, episode=episode.slug
))

return self._ok(data=list(data_generator()))

def delete(self, series_slug, episode_slug, **kwargs):
"""Delete the episode."""
if not series_slug:
return self._method_not_allowed('Deleting multiple series are not allowed')

identifier = SeriesIdentifier.from_slug(series_slug)
if not identifier:
return self._bad_request('Invalid series identifier')

series = Series.find_by_identifier(identifier)
if not series:
return self._not_found('Series not found')

episode_number = EpisodeNumber.from_slug(episode_slug)
if not episode_number:
return self._bad_request('Invalid episode number')

episode = Episode.find_by_series_and_episode(series, episode_number)
if not episode:
return self._not_found('Episode not found')

try:
episode.delete_episode()
except EpisodeDeletedException:
return self._no_content()
else:
return self._conflict('Unable to delete episode')
99 changes: 99 additions & 0 deletions medusa/server/api/v2/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# coding=utf-8
"""Request handler for alias (scene exceptions)."""
from __future__ import unicode_literals

from medusa import db

from medusa.server.api.v2.base import BaseRequestHandler
from medusa.providers.generic_provider import GenericProvider
from medusa.providers import get_provider_class
from medusa.tv.series import SeriesIdentifier
from os.path import basename
from medusa.common import statusStrings


class HistoryHandler(BaseRequestHandler):
"""History request handler."""

#: resource name
name = 'history'
#: identifier
identifier = ('series_slug', r'\w+')
#: path param
path_param = ('path_param', r'\w+')
#: allowed HTTP methods
allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')

def get(self, series_slug, path_param):
"""Query history information."""

sql_base = '''
SELECT rowid, date, action, quality,
provider, version, resource, size,
indexer_id, showid, season, episode
FROM history
'''
params = []

arg_page = self._get_page()
arg_limit = self._get_limit(default=50)

if series_slug is not None:
series_identifier = SeriesIdentifier.from_slug(series_slug)
if not series_identifier:
return self._bad_request('Invalid series')

sql_base += ' WHERE indexer_id = ? AND showid = ?'
params += [series_identifier.indexer.id, series_identifier.id]

sql_base += ' ORDER BY date DESC'
results = db.DBConnection().select(sql_base, params)

def data_generator():
"""Read log lines based on the specified criteria."""
start = arg_limit * (arg_page - 1) + 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you remove the +1 here you can remove the two -1 below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not sure. For ex. Page 1, with a Limit of 50. Will result in 1, page 2 will result in 51

Copy link
Contributor Author

@p0psicles p0psicles Apr 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed it for now.
It's not used anywhere right now. So I'll leave it at that.
When i'm picking up the vueify history page, i'll test it out.


for item in results[start - 1:start - 1 + arg_limit]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there are less results than limit? Possible list index out of range?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's possible, as it's slicing the results

d = {}
d['id'] = item['rowid']

if item['indexer_id'] and item['showid']:
d['series'] = SeriesIdentifier.from_id(item['indexer_id'], item['showid']).slug
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it's required. As for example for the history.mako page, we just want to get all history information.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But still indexer_id and showid must be in the DB?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ow yeah good point


d['status'] = item['action']
d['actionDate'] = item['date']

d['resource'] = basename(item['resource'])
d['size'] = item['size']
d['statusName'] = statusStrings.get(item['action'])
d['season'] = item['season']
d['episode'] = item['episode']
d['manuallySearched'] = bool(item['manually_searched'])

provider = get_provider_class(GenericProvider.make_id(item['provider']))
d['provider'] = {}
if provider:
d['provider']['id'] = provider.get_id()
d['provider']['name'] = provider.name
d['provider']['imageName'] = provider.image_name()

yield d

if not len(results):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

return self._not_found('History data not found')

return self._paginate(data_generator=data_generator)

def delete(self, identifier, **kwargs):
"""Delete a history record."""
identifier = self._parse(identifier)
if not identifier:
return self._bad_request('Invalid history id')

cache_db_con = db.DBConnection('cache.db')
last_changes = cache_db_con.connection.total_changes
cache_db_con.action('DELETE FROM history WHERE row_id = ?', [identifier])
if cache_db_con.connection.total_changes - last_changes <= 0:
return self._not_found('History row not found')

return self._no_content()
8 changes: 8 additions & 0 deletions medusa/server/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from medusa.server.api.v2.base import BaseRequestHandler, NotFoundHandler
from medusa.server.api.v2.config import ConfigHandler
from medusa.server.api.v2.episodes import EpisodeHandler
from medusa.server.api.v2.episode_history import EpisodeHistoryHandler
from medusa.server.api.v2.history import HistoryHandler
from medusa.server.api.v2.internal import InternalHandler
from medusa.server.api.v2.log import LogHandler
from medusa.server.api.v2.search import SearchHandler
Expand Down Expand Up @@ -80,6 +82,12 @@ def get_apiv2_handlers(base):

# Order: Most specific to most generic

# /api/v2/history/tvdb1234/episode
EpisodeHistoryHandler.create_app_handler(base),

# /api/v2/history
HistoryHandler.create_app_handler(base),

# /api/v2/search
SearchHandler.create_app_handler(base),

Expand Down
Loading