1
from datetime import datetime, timedelta
2
from flexget.manager import Session
3
from string import capwords
4
from sqlalchemy.orm import join
5
from sqlalchemy import desc
6
from flexget.plugin import register_plugin, register_parser_option, DependencyError
9
from flexget.plugins.filter.series import SeriesDatabase, Series, Episode, Release, forget_series, forget_series_episode
11
raise DependencyError(issued_by='cli_series', missing='series', message='Series commandline interface not loaded')
14
class SeriesReport(SeriesDatabase):
16
"""Produces --series report"""
21
def optik_series(option, opt, value, parser):
23
SeriesReport.options['got'] = True
25
SeriesReport.options['name'] = parser.rargs[0]
27
def on_process_start(self, feed):
29
feed.manager.disable_feeds()
31
if not 'name' in self.options:
32
self.display_summary()
34
self.display_details()
36
def display_details(self):
37
"""Display detailed series information, ie. --series NAME"""
39
from flexget.manager import Session
42
name = unicode(self.options['name'].lower())
43
series = session.query(Series).filter(Series.name == name).first()
45
print 'Unknown series `%s`' % name
48
print ' %-63s%-15s' % ('Identifier, Title', 'Quality')
51
# Query episodes in sane order instead of iterating from series.episodes
52
episodes = session.query(Episode).filter(Episode.series_id == series.id).\
53
order_by(Episode.identifier).all()
55
for episode in episodes:
57
if episode.identifier is None:
58
print ' None <--- Broken!'
60
print ' %s - %s' % (episode.identifier, episode.age)
62
for release in episode.releases:
63
status = release.quality.name
66
title = title[:55] + '...'
67
if release.proper_count > 0:
69
if release.proper_count > 1:
70
status += str(release.proper_count)
71
if release.downloaded:
72
print ' * %-60s%-15s' % (title, status)
74
print ' %-60s%-15s' % (title, status)
77
print ' * = downloaded'
80
def get_series_summary(self):
84
for series in session.query(Series).all():
85
name = unicode(series.name)
86
# capitalize if user hasn't, better look and sorting ...
89
result[name] = {'identified_by': series.identified_by}
90
episode = self.latest_seen_episode(session, series)
92
latest = {'first_seen': episode.first_seen,
93
'episode_instance': episode,
94
'episode_id': episode.identifier,
96
'status': self.get_latest_status(episode)}
97
result[name]['latest'] = latest
102
def get_latest_status(self, episode):
104
:param episode: Instance of Episode
105
:return: Status string for given episode
108
for release in sorted(episode.releases, key=lambda r: r.quality):
109
if release.downloaded:
111
status += release.quality.name
112
if release.proper_count > 0:
114
if release.proper_count > 1:
115
status += str(release.proper_count)
116
if release.downloaded:
119
return status if status else None
121
def latest_seen_episode(self, session, series):
123
:param session: SQLAlchemy session
124
:param series: Instance of Series
125
:return: Instance of latest Episode or None
127
if unicode(series.name).lower() == 'penn and teller':
130
# try to get latest episode in episodic format
131
if series.identified_by in ('ep', 'auto', None):
132
episode = session.query(Episode).select_from(join(Episode, Series)).\
133
filter(Series.id == series.id).\
134
filter(Episode.season != None).\
135
order_by(desc(Episode.season)).\
136
order_by(desc(Episode.number)).first()
137
# no luck, try uid format
138
if series.identified_by in ('id', 'auto', None):
140
episode = session.query(Episode).join(Series, Release).\
141
filter(Series.id == series.id).\
142
filter(Episode.season == None).\
143
order_by(desc(Release.first_seen)).first()
146
def display_summary(self, discontinued=False):
148
Display series summary. ie --series
149
:param discontinued: Whether to display active or discontinued series
152
formatting = ' %-30s %-10s %-10s %-20s'
153
print formatting % ('Name', 'Latest', 'Age', 'Status')
157
series = self.get_series_summary()
158
for series_name, data in sorted(series.iteritems()):
160
if len(series_name) > 30:
161
series_name = series_name[:27] + '...'
164
if data['latest']['first_seen'] > datetime.now() - timedelta(days=2):
166
if data['latest']['first_seen'] < datetime.now() - timedelta(days=30 * 7):
170
latest = data.get('latest', {})
171
status = latest.get('status', 'N/A')
172
age = latest.get('age', 'N/A')
173
episode_id = latest.get('episode_id', 'N/A')
175
print new_ep + formatting[1:] % (series_name, episode_id, age if age else '', status)
178
print ' [] = downloaded | > = new episode %s' % \
179
'| %i series unseen past 6 months hidden' % hidden if hidden else ''
180
print ' Use --series NAME to get detailed information'
183
class SeriesForget(object):
185
"""Provides --series-forget"""
190
def optik_series_forget(option, opt, value, parser):
193
--series-forget NAME [ID]
196
return # how to handle invalid?
197
if len(parser.rargs) > 0:
198
SeriesForget.options['name'] = parser.rargs[0]
199
if len(parser.rargs) > 1:
200
SeriesForget.options['episode'] = parser.rargs[1]
202
def on_process_start(self, feed):
204
feed.manager.disable_feeds()
206
name = unicode(self.options.get('name'))
208
if self.options.get('episode'):
210
identifier = self.options.get('episode').upper()
211
if identifier and name:
213
forget_series_episode(name, identifier)
214
print 'Removed episode `%s` from series `%s`.' % (identifier, name.capitalize())
215
except ValueError, e:
218
# remove whole series
221
print 'Removed series `%s` from database.' % name.capitalize()
222
except ValueError, e:
226
register_plugin(SeriesReport, '--series', builtin=True)
227
register_plugin(SeriesForget, '--series-forget', builtin=True)
229
register_parser_option('--series', action='callback', callback=SeriesReport.optik_series,
230
help='Display series summary.')
231
register_parser_option('--series-forget', action='callback', callback=SeriesForget.optik_series_forget,
232
help='Remove complete series or single episode from database: <NAME> [EPISODE]')