episoder 0.11.0__tar.gz → 0.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {episoder-0.11.0 → episoder-0.11.1}/.gitlab-ci.yml +17 -0
- {episoder-0.11.0 → episoder-0.11.1}/CHANGELOG +3 -0
- {episoder-0.11.0 → episoder-0.11.1}/PKG-INFO +2 -2
- {episoder-0.11.0 → episoder-0.11.1}/episoder/database.py +1 -1
- {episoder-0.11.0 → episoder-0.11.1}/episoder/main.py +9 -29
- episoder-0.11.1/episoder/output.py +169 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder/version.py +1 -1
- {episoder-0.11.0 → episoder-0.11.1}/episoder.egg-info/PKG-INFO +2 -2
- {episoder-0.11.0 → episoder-0.11.1}/test/test_notify.py +6 -45
- {episoder-0.11.0 → episoder-0.11.1}/test/test_output.py +52 -79
- episoder-0.11.0/episoder/output.py +0 -186
- {episoder-0.11.0 → episoder-0.11.1}/.gitignore +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/COPYING +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/README.md +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder/__init__.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder/episoder.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder/sources/__init__.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder/sources/epguides.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder/sources/parser.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder/sources/thetvdb.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder/sources/tvcom.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder.1 +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder.egg-info/SOURCES.txt +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder.egg-info/dependency_links.txt +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder.egg-info/entry_points.txt +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder.egg-info/requires.txt +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/episoder.egg-info/top_level.txt +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/examples/home.episoder +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/pyproject.toml +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/setup.cfg +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/setup.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/__init__.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_48_hours_mistery.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_american_idol.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_bsg.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_buzzcocks.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_doctor_who.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_eureka.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_futurama.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_house.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_kr2008.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_lost.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_lost_html.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_lower_decks.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_midsomer_murders.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_picard.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/epguides_test_iso_8859_1.html +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_error_404.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_search_frasier.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_search_friends.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_260.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_260_1_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_261.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_261_1_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_261_2_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_261_3_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_262.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_262_1_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_263.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_268156.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_268156_1_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_295640.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_295640_1_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_295640_2_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_73739.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_73739_1_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_75397.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/fixtures/tvdb_test_show_75397_1_eps.json +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/test_database.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/test_epguides_parser.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/test_episode.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/test_metainfo.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/test_migration.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/test_show.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/test_sources.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/test_tvcom.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/test_tvdb.py +0 -0
- {episoder-0.11.0 → episoder-0.11.1}/test/util.py +0 -0
|
@@ -8,6 +8,9 @@ stages:
|
|
|
8
8
|
.bookworm_image: &bookworm
|
|
9
9
|
image: registry.gitlab.com/schabe/episoder:debian-bookworm
|
|
10
10
|
|
|
11
|
+
.trixie_image: &trixie
|
|
12
|
+
image: registry.gitlab.com/schabe/episoder:debian-trixie
|
|
13
|
+
|
|
11
14
|
.noble_image: &noble
|
|
12
15
|
image: registry.gitlab.com/schabe/episoder:ubuntu-noble
|
|
13
16
|
|
|
@@ -105,3 +108,17 @@ py312-noble-pip:deploy:
|
|
|
105
108
|
needs: ["py312-noble:test"]
|
|
106
109
|
<<: *noble
|
|
107
110
|
<<: *deploy_pip_venv
|
|
111
|
+
|
|
112
|
+
py313-trixie:test:
|
|
113
|
+
<<: *trixie
|
|
114
|
+
<<: *test
|
|
115
|
+
|
|
116
|
+
py313-trixie-setuptools:deploy:
|
|
117
|
+
needs: ["py313-trixie:test"]
|
|
118
|
+
<<: *trixie
|
|
119
|
+
<<: *deploy_setuptools
|
|
120
|
+
|
|
121
|
+
py313-trixie-pip:deploy:
|
|
122
|
+
needs: ["py313-trixie:test"]
|
|
123
|
+
<<: *trixie
|
|
124
|
+
<<: *deploy_pip_venv
|
|
@@ -83,7 +83,7 @@ class Episode(Base):
|
|
|
83
83
|
notified = Column(Date)
|
|
84
84
|
show = relationship(Show, backref=backref("episodes", cascade="all"))
|
|
85
85
|
|
|
86
|
-
# pylint: disable=too-many-arguments
|
|
86
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
87
87
|
def __init__(self, name, season, episode, aired, prodnum, total):
|
|
88
88
|
|
|
89
89
|
self.title = name
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
+
import sys
|
|
21
22
|
|
|
22
23
|
from argparse import ArgumentParser
|
|
23
24
|
from datetime import date, timedelta
|
|
@@ -30,7 +31,7 @@ from episoder.database import Show
|
|
|
30
31
|
from episoder.episoder import Database
|
|
31
32
|
from episoder.sources import parser_for, setup_sources
|
|
32
33
|
from episoder.sources.thetvdb import TVDBShowNotFoundError
|
|
33
|
-
from episoder.output import
|
|
34
|
+
from episoder.output import ColorfulRenderer, ColorlessRenderer, EmailNotifier
|
|
34
35
|
from episoder.output import NewEpisodesNotification
|
|
35
36
|
from episoder.version import __version__
|
|
36
37
|
|
|
@@ -38,7 +39,6 @@ from episoder.version import __version__
|
|
|
38
39
|
class EpisoderOptions:
|
|
39
40
|
|
|
40
41
|
def __init__(self):
|
|
41
|
-
|
|
42
42
|
self._parser = ArgumentParser()
|
|
43
43
|
self._add_options()
|
|
44
44
|
self._args = None
|
|
@@ -46,7 +46,6 @@ class EpisoderOptions:
|
|
|
46
46
|
self.loglevel = logging.WARNING
|
|
47
47
|
|
|
48
48
|
def _add_options(self):
|
|
49
|
-
|
|
50
49
|
yesterday = date.today() - timedelta(1)
|
|
51
50
|
|
|
52
51
|
self._parser.set_defaults(func=None)
|
|
@@ -162,7 +161,6 @@ class EpisoderOptions:
|
|
|
162
161
|
help="show version information")
|
|
163
162
|
|
|
164
163
|
def _replace_parsed_date(self, args):
|
|
165
|
-
|
|
166
164
|
if args.date.isdigit():
|
|
167
165
|
daysback = int(args.date)
|
|
168
166
|
args.date = date.today() - timedelta(daysback)
|
|
@@ -173,7 +171,6 @@ class EpisoderOptions:
|
|
|
173
171
|
self._parser.error(f"{args.date}: Wrong date format")
|
|
174
172
|
|
|
175
173
|
def _setup_logging(self):
|
|
176
|
-
|
|
177
174
|
if self._args.debug:
|
|
178
175
|
self.loglevel = logging.DEBUG
|
|
179
176
|
elif self._args.verbose:
|
|
@@ -188,14 +185,13 @@ class EpisoderOptions:
|
|
|
188
185
|
logging.basicConfig(level=level)
|
|
189
186
|
|
|
190
187
|
def parse_args(self):
|
|
191
|
-
|
|
192
188
|
self._args = self._parser.parse_args()
|
|
193
189
|
|
|
194
190
|
# this happens automatically in python 2 but not in 3.4
|
|
195
191
|
if self._args.func is None:
|
|
196
192
|
self._parser.print_usage()
|
|
197
193
|
stderr.write("error: too few arguments\n")
|
|
198
|
-
exit(1)
|
|
194
|
+
sys.exit(1)
|
|
199
195
|
|
|
200
196
|
# turn our date string into a proper date object
|
|
201
197
|
if hasattr(self._args, "date"):
|
|
@@ -206,11 +202,9 @@ class EpisoderOptions:
|
|
|
206
202
|
def load_rcfile(self, file_):
|
|
207
203
|
|
|
208
204
|
def strip_comments(line):
|
|
209
|
-
|
|
210
205
|
return line.split("#")[0]
|
|
211
206
|
|
|
212
207
|
def valid(line):
|
|
213
|
-
|
|
214
208
|
return "=" in line
|
|
215
209
|
|
|
216
210
|
self._log.info("Loading config file")
|
|
@@ -221,7 +215,6 @@ class EpisoderOptions:
|
|
|
221
215
|
return dict(line.split("=") for line in lines)
|
|
222
216
|
|
|
223
217
|
def load_config(self):
|
|
224
|
-
|
|
225
218
|
if path.exists(self.args.c):
|
|
226
219
|
with open(self.args.c, "rb") as file_:
|
|
227
220
|
cfg = self.load_rcfile(file_)
|
|
@@ -251,21 +244,17 @@ class EpisoderOptions:
|
|
|
251
244
|
self._log.info("Loaded configuration")
|
|
252
245
|
|
|
253
246
|
def _get_args(self):
|
|
254
|
-
|
|
255
247
|
return self._args
|
|
256
248
|
|
|
257
249
|
args = property(_get_args)
|
|
258
250
|
|
|
259
251
|
|
|
260
252
|
class Episoder:
|
|
261
|
-
|
|
262
253
|
def __init__(self, args):
|
|
263
|
-
|
|
264
254
|
self._args = args
|
|
265
255
|
self._log = logging.getLogger("Episoder")
|
|
266
256
|
|
|
267
257
|
def update(self):
|
|
268
|
-
|
|
269
258
|
database = Database(self._args.datafile)
|
|
270
259
|
database.migrate()
|
|
271
260
|
|
|
@@ -283,7 +272,6 @@ class Episoder:
|
|
|
283
272
|
shows = database.get_expired_shows()
|
|
284
273
|
|
|
285
274
|
for show in shows[0:self._args.num]:
|
|
286
|
-
|
|
287
275
|
try:
|
|
288
276
|
parser = parser_for(show.url)
|
|
289
277
|
parser.parse(show, database)
|
|
@@ -300,7 +288,6 @@ class Episoder:
|
|
|
300
288
|
self._log.info("None of your shows need to be updated")
|
|
301
289
|
|
|
302
290
|
def add(self):
|
|
303
|
-
|
|
304
291
|
url = self._args.show
|
|
305
292
|
|
|
306
293
|
database = Database(self._args.datafile)
|
|
@@ -322,14 +309,12 @@ class Episoder:
|
|
|
322
309
|
database.commit()
|
|
323
310
|
|
|
324
311
|
def remove(self):
|
|
325
|
-
|
|
326
312
|
database = Database(self._args.datafile)
|
|
327
313
|
database.migrate()
|
|
328
314
|
database.remove_show(self._args.show)
|
|
329
315
|
database.commit()
|
|
330
316
|
|
|
331
317
|
def _set_show_enabled(self, database, enabled):
|
|
332
|
-
|
|
333
318
|
show_id = self._args.show
|
|
334
319
|
show = database.get_show_by_id(show_id)
|
|
335
320
|
|
|
@@ -341,19 +326,16 @@ class Episoder:
|
|
|
341
326
|
database.commit()
|
|
342
327
|
|
|
343
328
|
def enable(self):
|
|
344
|
-
|
|
345
329
|
database = Database(self._args.datafile)
|
|
346
330
|
database.migrate()
|
|
347
331
|
self._set_show_enabled(database, True)
|
|
348
332
|
|
|
349
333
|
def disable(self):
|
|
350
|
-
|
|
351
334
|
database = Database(self._args.datafile)
|
|
352
335
|
database.migrate()
|
|
353
336
|
self._set_show_enabled(database, False)
|
|
354
337
|
|
|
355
338
|
def shows(self):
|
|
356
|
-
|
|
357
339
|
database = Database(self._args.datafile)
|
|
358
340
|
database.migrate()
|
|
359
341
|
|
|
@@ -374,7 +356,6 @@ class Episoder:
|
|
|
374
356
|
print(f" Episodes: {len(show.episodes)}")
|
|
375
357
|
|
|
376
358
|
def notify(self):
|
|
377
|
-
|
|
378
359
|
opt = self._args
|
|
379
360
|
|
|
380
361
|
if opt.email_to is None:
|
|
@@ -406,9 +387,11 @@ class Episoder:
|
|
|
406
387
|
database.commit()
|
|
407
388
|
|
|
408
389
|
def episodes(self):
|
|
409
|
-
|
|
410
390
|
database = Database(self._args.datafile)
|
|
411
|
-
|
|
391
|
+
if self._args.nocolor:
|
|
392
|
+
out = ColorlessRenderer(self._args.format, self._args.dateformat)
|
|
393
|
+
else:
|
|
394
|
+
out = ConsoleRenderer(self._args.format, self._args.dateformat)
|
|
412
395
|
|
|
413
396
|
if self._args.nodate:
|
|
414
397
|
startdate = date(1900, 1, 1)
|
|
@@ -422,10 +405,9 @@ class Episoder:
|
|
|
422
405
|
else:
|
|
423
406
|
episodes = database.get_episodes(startdate, n_days)
|
|
424
407
|
|
|
425
|
-
out.render(episodes, self._args.date
|
|
408
|
+
out.render(episodes, self._args.date)
|
|
426
409
|
|
|
427
410
|
def search(self):
|
|
428
|
-
|
|
429
411
|
tvdb = parser_for('123') # numeric IDs are TVDB entries
|
|
430
412
|
|
|
431
413
|
try:
|
|
@@ -437,7 +419,6 @@ class Episoder:
|
|
|
437
419
|
print("Nothing found")
|
|
438
420
|
|
|
439
421
|
def check_data_file(self):
|
|
440
|
-
|
|
441
422
|
filename = self._args.datafile
|
|
442
423
|
if not path.exists(filename):
|
|
443
424
|
return
|
|
@@ -448,11 +429,10 @@ class Episoder:
|
|
|
448
429
|
if data != b"SQLite":
|
|
449
430
|
message = "%s: Please remove old data file\n"
|
|
450
431
|
stderr.write(message % filename)
|
|
451
|
-
exit(4)
|
|
432
|
+
sys.exit(4)
|
|
452
433
|
|
|
453
434
|
|
|
454
435
|
def main():
|
|
455
|
-
|
|
456
436
|
options = EpisoderOptions()
|
|
457
437
|
options.parse_args()
|
|
458
438
|
options.load_config()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# episoder, https://code.ott.net/episoder
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2004-2025 Stefan Ott. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
from datetime import date, timedelta
|
|
19
|
+
from email.message import EmailMessage
|
|
20
|
+
from smtplib import SMTP
|
|
21
|
+
from sys import stdout
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from .database import Episode
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# pylint: disable=too-few-public-methods
|
|
28
|
+
class EpisodeRenderer:
|
|
29
|
+
def __init__(self, episode_format: str, date_format: str) -> None:
|
|
30
|
+
self._format = episode_format
|
|
31
|
+
self._date_format = date_format
|
|
32
|
+
|
|
33
|
+
def format(self, episode: Episode) -> str:
|
|
34
|
+
string = self._format
|
|
35
|
+
|
|
36
|
+
airdate = episode.airdate.strftime(self._date_format)
|
|
37
|
+
string = string.replace("%airdate", airdate)
|
|
38
|
+
string = string.replace("%show", str(episode.show.name))
|
|
39
|
+
string = string.replace("%season", str(episode.season))
|
|
40
|
+
string = string.replace("%epnum", f"{episode.episode:02d}")
|
|
41
|
+
string = string.replace("%eptitle", str(episode.title))
|
|
42
|
+
string = string.replace("%totalep", str(episode.totalnum))
|
|
43
|
+
string = string.replace("%prodnum", str(episode.prodnum))
|
|
44
|
+
|
|
45
|
+
return string
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ColorfulRenderer(EpisodeRenderer):
|
|
49
|
+
RED = "\033[31;1m"
|
|
50
|
+
CYAN = "\033[36;1m"
|
|
51
|
+
GREY = "\033[30;0m"
|
|
52
|
+
GREEN = "\033[32;1m"
|
|
53
|
+
YELLOW = "\033[33;1m"
|
|
54
|
+
|
|
55
|
+
def __init__(self, fmt: str, date_format: str, dest=stdout) -> None:
|
|
56
|
+
super().__init__(fmt, date_format)
|
|
57
|
+
self._dest = dest
|
|
58
|
+
|
|
59
|
+
def _color(self, episode: Episode, yesterday: date, today: date,
|
|
60
|
+
tomorrow: date) -> str:
|
|
61
|
+
if episode.airdate == yesterday:
|
|
62
|
+
return ColorfulRenderer.RED
|
|
63
|
+
if episode.airdate == today:
|
|
64
|
+
return ColorfulRenderer.YELLOW
|
|
65
|
+
if episode.airdate == tomorrow:
|
|
66
|
+
return ColorfulRenderer.GREEN
|
|
67
|
+
if episode.airdate > tomorrow:
|
|
68
|
+
return ColorfulRenderer.CYAN
|
|
69
|
+
|
|
70
|
+
return ColorfulRenderer.GREY
|
|
71
|
+
|
|
72
|
+
def render(self, episodes: list[Episode], today: date) -> None:
|
|
73
|
+
yesterday = today - timedelta(1)
|
|
74
|
+
tomorrow = today + timedelta(1)
|
|
75
|
+
|
|
76
|
+
for episode in episodes:
|
|
77
|
+
text = self.format(episode)
|
|
78
|
+
color = self._color(episode, yesterday, today, tomorrow)
|
|
79
|
+
self._dest.write(f'{color}{text}{ColorfulRenderer.GREY}\n')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ColorlessRenderer(EpisodeRenderer):
|
|
83
|
+
def __init__(self, fmt: str, date_format: str, dest=stdout) -> None:
|
|
84
|
+
super().__init__(fmt, date_format)
|
|
85
|
+
self._dest = dest
|
|
86
|
+
|
|
87
|
+
def render(self, episodes: list[Episode], _: date) -> None:
|
|
88
|
+
for episode in episodes:
|
|
89
|
+
text = self.format(episode)
|
|
90
|
+
self._dest.write(f'{text}\n')
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class EmailNotifier:
|
|
94
|
+
def __init__(self, server: str, port: int, smtp=SMTP) -> None:
|
|
95
|
+
self._server = server
|
|
96
|
+
self._port = port
|
|
97
|
+
self._smtp = smtp
|
|
98
|
+
self._user: Optional[str] = None
|
|
99
|
+
self._password: Optional[str] = None
|
|
100
|
+
|
|
101
|
+
self.use_tls = False
|
|
102
|
+
|
|
103
|
+
def __str__(self) -> str:
|
|
104
|
+
return "EmailNotifier"
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
fmt = 'EmailNotifier("%s", %d, %s)'
|
|
108
|
+
return fmt % (self._server, self._port, str(self._smtp))
|
|
109
|
+
|
|
110
|
+
def login(self, user: str, password: str) -> None:
|
|
111
|
+
self._user = user
|
|
112
|
+
self._password = password
|
|
113
|
+
|
|
114
|
+
def _dump(self, msg: EmailMessage) -> None:
|
|
115
|
+
print(f'From: {msg["to"]}')
|
|
116
|
+
print(f'To: {msg["to"]}')
|
|
117
|
+
print(f'Subject: {msg["Subject"]}')
|
|
118
|
+
print(msg.get_content())
|
|
119
|
+
|
|
120
|
+
def send(self, body: str, to: str, pretend: bool = False) -> None:
|
|
121
|
+
message = EmailMessage()
|
|
122
|
+
message.set_content(body)
|
|
123
|
+
message['Subject'] = 'Upcoming TV episodes'
|
|
124
|
+
message['From'] = to
|
|
125
|
+
message['To'] = to
|
|
126
|
+
|
|
127
|
+
if pretend:
|
|
128
|
+
self._dump(message)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
server = self._smtp(self._server, self._port)
|
|
132
|
+
|
|
133
|
+
if self.use_tls:
|
|
134
|
+
server.starttls()
|
|
135
|
+
if self._user:
|
|
136
|
+
server.login(self._user, self._password)
|
|
137
|
+
|
|
138
|
+
server.send_message(message)
|
|
139
|
+
server.quit()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class NewEpisodesNotification(EpisodeRenderer):
|
|
143
|
+
def __init__(self, episodes: list[Episode], fmt: str,
|
|
144
|
+
date_format: str) -> None:
|
|
145
|
+
super().__init__(fmt, date_format)
|
|
146
|
+
self._episodes = episodes
|
|
147
|
+
|
|
148
|
+
def __str__(self) -> str:
|
|
149
|
+
return f'Notification: {len(self._episodes)} new episode(s)'
|
|
150
|
+
|
|
151
|
+
def __repr__(self) -> str:
|
|
152
|
+
eps = f"<({len(self._episodes)} x Episode)"
|
|
153
|
+
fmt = 'NewEpisodesNotification(%s, "%s", "%s")'
|
|
154
|
+
return fmt % (eps, self._format, self._date_format)
|
|
155
|
+
|
|
156
|
+
def get(self) -> str:
|
|
157
|
+
return ''.join(f'{self.format(e)}x\n' for e in self._episodes)
|
|
158
|
+
|
|
159
|
+
def send(self, notifier: EmailNotifier, to: str,
|
|
160
|
+
pretend: bool = False) -> None:
|
|
161
|
+
body = "Your upcoming episodes:\n\n"
|
|
162
|
+
|
|
163
|
+
for episode in self._episodes:
|
|
164
|
+
body += f"{self.format(episode)}\n"
|
|
165
|
+
|
|
166
|
+
if not pretend:
|
|
167
|
+
episode.notified = date.today()
|
|
168
|
+
|
|
169
|
+
notifier.send(body, to, pretend)
|
|
@@ -31,11 +31,9 @@ from episoder.output import EmailNotifier, NewEpisodesNotification
|
|
|
31
31
|
|
|
32
32
|
# pylint: disable=too-many-instance-attributes
|
|
33
33
|
class MockSMTPInstance():
|
|
34
|
-
|
|
35
34
|
instance = None
|
|
36
35
|
|
|
37
36
|
def __init__(self, server, port):
|
|
38
|
-
|
|
39
37
|
self.server = server
|
|
40
38
|
self.port = port
|
|
41
39
|
|
|
@@ -49,99 +47,78 @@ class MockSMTPInstance():
|
|
|
49
47
|
self.msg = None
|
|
50
48
|
|
|
51
49
|
def reset(self):
|
|
52
|
-
|
|
53
50
|
self.count = 0
|
|
54
51
|
self.data = {}
|
|
55
52
|
self.tls = 0
|
|
56
53
|
self.user = None
|
|
57
54
|
self.password = None
|
|
58
55
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
self.data[
|
|
62
|
-
self.data[
|
|
63
|
-
self.data["msg"] = msg
|
|
56
|
+
def send_message(self, msg):
|
|
57
|
+
self.data['from'] = msg['from']
|
|
58
|
+
self.data['to'] = msg['to']
|
|
59
|
+
self.data['msg'] = msg.get_content()
|
|
64
60
|
|
|
65
61
|
def quit(self):
|
|
66
|
-
|
|
67
62
|
self.from_addr = self.data.get("from")
|
|
68
63
|
self.to_addr = self.data.get("to")
|
|
69
64
|
self.msg = self.data.get("msg")
|
|
70
65
|
self.count += 1
|
|
71
66
|
|
|
72
67
|
def starttls(self):
|
|
73
|
-
|
|
74
68
|
self.tls = 1
|
|
75
69
|
|
|
76
70
|
def login(self, user, password):
|
|
77
|
-
|
|
78
71
|
self.user = user
|
|
79
72
|
self.password = password
|
|
80
73
|
|
|
81
74
|
|
|
82
75
|
class MockSMTP():
|
|
83
|
-
|
|
84
76
|
instance = None
|
|
85
77
|
|
|
86
78
|
def __init__(self, server, port):
|
|
87
|
-
|
|
88
79
|
if not MockSMTP.instance:
|
|
89
80
|
MockSMTP.instance = MockSMTPInstance(server, port)
|
|
90
81
|
|
|
91
82
|
def __str__(self):
|
|
92
|
-
|
|
93
83
|
return "MockSMTP"
|
|
94
84
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
self.instance.sendmail(from_addr, to_addr, msg)
|
|
85
|
+
def send_message(self, msg):
|
|
86
|
+
self.instance.send_message(msg)
|
|
98
87
|
|
|
99
88
|
def quit(self):
|
|
100
|
-
|
|
101
89
|
self.instance.quit()
|
|
102
90
|
|
|
103
91
|
def _get_count(self):
|
|
104
|
-
|
|
105
92
|
return MockSMTP.instance.count
|
|
106
93
|
|
|
107
94
|
def _set_count(self, num):
|
|
108
|
-
|
|
109
95
|
MockSMTP.instance.count = num
|
|
110
96
|
|
|
111
97
|
def _get_from(self):
|
|
112
|
-
|
|
113
98
|
return MockSMTP.instance.from_addr
|
|
114
99
|
|
|
115
100
|
def _get_to(self):
|
|
116
|
-
|
|
117
101
|
return MockSMTP.instance.to_addr
|
|
118
102
|
|
|
119
103
|
def _get_msg(self):
|
|
120
|
-
|
|
121
104
|
return MockSMTP.instance.msg
|
|
122
105
|
|
|
123
106
|
def _get_tls(self):
|
|
124
|
-
|
|
125
107
|
return MockSMTP.instance.tls
|
|
126
108
|
|
|
127
109
|
def _get_user(self):
|
|
128
|
-
|
|
129
110
|
return MockSMTP.instance.user
|
|
130
111
|
|
|
131
112
|
def _get_password(self):
|
|
132
|
-
|
|
133
113
|
return MockSMTP.instance.password
|
|
134
114
|
|
|
135
115
|
def starttls(self):
|
|
136
|
-
|
|
137
116
|
MockSMTP.instance.starttls()
|
|
138
117
|
|
|
139
118
|
def login(self, user, password):
|
|
140
|
-
|
|
141
119
|
MockSMTP.instance.login(user, password)
|
|
142
120
|
|
|
143
121
|
def reset(self):
|
|
144
|
-
|
|
145
122
|
MockSMTP.instance.reset()
|
|
146
123
|
|
|
147
124
|
count = property(_get_count, _set_count)
|
|
@@ -154,9 +131,7 @@ class MockSMTP():
|
|
|
154
131
|
|
|
155
132
|
|
|
156
133
|
class TestNotification(TestCase):
|
|
157
|
-
|
|
158
134
|
def setUp(self):
|
|
159
|
-
|
|
160
135
|
self.show = Show("Test show 36")
|
|
161
136
|
self.show.show_id = 36
|
|
162
137
|
|
|
@@ -168,7 +143,6 @@ class TestNotification(TestCase):
|
|
|
168
143
|
self.smtp.reset()
|
|
169
144
|
|
|
170
145
|
def test_str_and_repr(self):
|
|
171
|
-
|
|
172
146
|
notify = EmailNotifier("localhost", 25, MockSMTP)
|
|
173
147
|
self.assertEqual(str(notify), "EmailNotifier")
|
|
174
148
|
self.assertEqual(repr(notify), 'EmailNotifier("localhost", 25, '
|
|
@@ -186,7 +160,6 @@ class TestNotification(TestCase):
|
|
|
186
160
|
self.assertEqual(repr(message), expect)
|
|
187
161
|
|
|
188
162
|
def test_send_mail_pretend(self):
|
|
189
|
-
|
|
190
163
|
show1 = Show("Columbo", "")
|
|
191
164
|
ep1 = Episode("Episode 1", 1, 1, date(2014, 8, 10), "x", 1)
|
|
192
165
|
ep2 = Episode("Episode 2", 1, 2, date(2014, 12, 1), "x", 2)
|
|
@@ -218,7 +191,6 @@ class TestNotification(TestCase):
|
|
|
218
191
|
"[2014-12-01] Columbo 1x02 - Episode 2\n\n")
|
|
219
192
|
|
|
220
193
|
def test_send_mail(self):
|
|
221
|
-
|
|
222
194
|
show1 = Show("Columbo", "")
|
|
223
195
|
ep1 = Episode("Episode 1", 1, 1, date(2014, 8, 10), "x", 1)
|
|
224
196
|
ep2 = Episode("Episode 2", 1, 2, date(2014, 12, 1), "x", 2)
|
|
@@ -242,9 +214,6 @@ class TestNotification(TestCase):
|
|
|
242
214
|
self.assertEqual(self.smtp.from_addr, "person@example.org")
|
|
243
215
|
self.assertEqual(self.smtp.to_addr, "person@example.org")
|
|
244
216
|
self.assertEqual(self.smtp.msg,
|
|
245
|
-
"From: person@example.org\n"
|
|
246
|
-
"To: person@example.org\n"
|
|
247
|
-
"Subject: Upcoming TV episodes\n"
|
|
248
217
|
"Your upcoming episodes:\n\n"
|
|
249
218
|
"[2014-08-10] Columbo 1x01 - Episode 1\n"
|
|
250
219
|
"[2014-12-01] Columbo 1x02 - Episode 2\n")
|
|
@@ -253,7 +222,6 @@ class TestNotification(TestCase):
|
|
|
253
222
|
self.assertIsNotNone(ep2.notified)
|
|
254
223
|
|
|
255
224
|
def test_send_mail_tls(self):
|
|
256
|
-
|
|
257
225
|
show1 = Show("Columbo", "")
|
|
258
226
|
ep1 = Episode("Episode 1", 1, 1, date(2014, 8, 10), "x", 1)
|
|
259
227
|
ep2 = Episode("Episode 2", 1, 2, date(2014, 12, 1), "x", 2)
|
|
@@ -276,15 +244,11 @@ class TestNotification(TestCase):
|
|
|
276
244
|
self.assertEqual(self.smtp.from_addr, "xy@example.org")
|
|
277
245
|
self.assertEqual(self.smtp.to_addr, "xy@example.org")
|
|
278
246
|
self.assertEqual(self.smtp.msg,
|
|
279
|
-
"From: xy@example.org\n"
|
|
280
|
-
"To: xy@example.org\n"
|
|
281
|
-
"Subject: Upcoming TV episodes\n"
|
|
282
247
|
"Your upcoming episodes:\n\n"
|
|
283
248
|
"[2014-08-10] Columbo 1x01 - Episode 1\n"
|
|
284
249
|
"[2014-12-01] Columbo 1x02 - Episode 2\n")
|
|
285
250
|
|
|
286
251
|
def test_send_mail_auth(self):
|
|
287
|
-
|
|
288
252
|
message = NewEpisodesNotification([], "", "")
|
|
289
253
|
|
|
290
254
|
notifier = EmailNotifier("localhost", 97, MockSMTP)
|
|
@@ -298,7 +262,4 @@ class TestNotification(TestCase):
|
|
|
298
262
|
self.assertEqual(self.smtp.from_addr, "xy@example.org")
|
|
299
263
|
self.assertEqual(self.smtp.to_addr, "xy@example.org")
|
|
300
264
|
self.assertEqual(self.smtp.msg,
|
|
301
|
-
"From: xy@example.org\n"
|
|
302
|
-
"To: xy@example.org\n"
|
|
303
|
-
"Subject: Upcoming TV episodes\n"
|
|
304
265
|
"Your upcoming episodes:\n\n")
|