episoder 0.11.1__tar.gz → 0.12.0__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.
Files changed (86) hide show
  1. {episoder-0.11.1 → episoder-0.12.0}/.gitlab-ci.yml +32 -6
  2. {episoder-0.11.1 → episoder-0.12.0}/CHANGELOG +6 -0
  3. {episoder-0.11.1 → episoder-0.12.0}/PKG-INFO +4 -1
  4. episoder-0.12.0/episoder/config.py +102 -0
  5. {episoder-0.11.1 → episoder-0.12.0}/episoder/database.py +4 -10
  6. {episoder-0.11.1 → episoder-0.12.0}/episoder/episoder.py +44 -75
  7. episoder-0.12.0/episoder/main.py +377 -0
  8. episoder-0.12.0/episoder/options.py +176 -0
  9. episoder-0.12.0/episoder/output.py +153 -0
  10. {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/epguides.py +9 -7
  11. {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/parser.py +3 -2
  12. {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/thetvdb.py +8 -7
  13. {episoder-0.11.1 → episoder-0.12.0}/episoder/version.py +2 -2
  14. {episoder-0.11.1 → episoder-0.12.0}/episoder.1 +5 -2
  15. {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/PKG-INFO +4 -1
  16. {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/SOURCES.txt +6 -0
  17. {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/requires.txt +4 -0
  18. {episoder-0.11.1 → episoder-0.12.0}/setup.cfg +5 -0
  19. episoder-0.12.0/test/test_commands.py +220 -0
  20. episoder-0.12.0/test/test_config.py +135 -0
  21. {episoder-0.11.1 → episoder-0.12.0}/test/test_database.py +140 -139
  22. {episoder-0.11.1 → episoder-0.12.0}/test/test_epguides_parser.py +113 -108
  23. {episoder-0.11.1 → episoder-0.12.0}/test/test_episode.py +12 -12
  24. episoder-0.12.0/test/test_main.py +911 -0
  25. {episoder-0.11.1 → episoder-0.12.0}/test/test_migration.py +22 -22
  26. episoder-0.12.0/test/test_notify.py +129 -0
  27. episoder-0.12.0/test/test_options.py +427 -0
  28. {episoder-0.11.1 → episoder-0.12.0}/test/test_output.py +61 -61
  29. {episoder-0.11.1 → episoder-0.12.0}/test/test_show.py +12 -12
  30. {episoder-0.11.1 → episoder-0.12.0}/test/test_tvdb.py +29 -29
  31. {episoder-0.11.1 → episoder-0.12.0}/test/util.py +3 -9
  32. episoder-0.11.1/episoder/main.py +0 -449
  33. episoder-0.11.1/episoder/output.py +0 -169
  34. episoder-0.11.1/test/test_notify.py +0 -265
  35. {episoder-0.11.1 → episoder-0.12.0}/.gitignore +0 -0
  36. {episoder-0.11.1 → episoder-0.12.0}/COPYING +0 -0
  37. {episoder-0.11.1 → episoder-0.12.0}/README.md +0 -0
  38. {episoder-0.11.1 → episoder-0.12.0}/episoder/__init__.py +0 -0
  39. {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/__init__.py +0 -0
  40. {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/tvcom.py +0 -0
  41. {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/dependency_links.txt +0 -0
  42. {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/entry_points.txt +0 -0
  43. {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/top_level.txt +0 -0
  44. {episoder-0.11.1 → episoder-0.12.0}/examples/home.episoder +0 -0
  45. {episoder-0.11.1 → episoder-0.12.0}/pyproject.toml +0 -0
  46. {episoder-0.11.1 → episoder-0.12.0}/setup.py +0 -0
  47. {episoder-0.11.1 → episoder-0.12.0}/test/__init__.py +0 -0
  48. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_48_hours_mistery.html +0 -0
  49. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_american_idol.html +0 -0
  50. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_bsg.html +0 -0
  51. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_buzzcocks.html +0 -0
  52. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_doctor_who.html +0 -0
  53. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_eureka.html +0 -0
  54. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_futurama.html +0 -0
  55. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_house.html +0 -0
  56. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_kr2008.html +0 -0
  57. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_lost.html +0 -0
  58. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_lost_html.html +0 -0
  59. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_lower_decks.html +0 -0
  60. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_midsomer_murders.html +0 -0
  61. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_picard.html +0 -0
  62. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_test_iso_8859_1.html +0 -0
  63. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_error_404.json +0 -0
  64. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_search_frasier.json +0 -0
  65. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_search_friends.json +0 -0
  66. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_260.json +0 -0
  67. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_260_1_eps.json +0 -0
  68. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_261.json +0 -0
  69. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_261_1_eps.json +0 -0
  70. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_261_2_eps.json +0 -0
  71. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_261_3_eps.json +0 -0
  72. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_262.json +0 -0
  73. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_262_1_eps.json +0 -0
  74. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_263.json +0 -0
  75. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_268156.json +0 -0
  76. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_268156_1_eps.json +0 -0
  77. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_295640.json +0 -0
  78. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_295640_1_eps.json +0 -0
  79. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_295640_2_eps.json +0 -0
  80. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_73739.json +0 -0
  81. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_73739_1_eps.json +0 -0
  82. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_75397.json +0 -0
  83. {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_75397_1_eps.json +0 -0
  84. {episoder-0.11.1 → episoder-0.12.0}/test/test_metainfo.py +0 -0
  85. {episoder-0.11.1 → episoder-0.12.0}/test/test_sources.py +0 -0
  86. {episoder-0.11.1 → episoder-0.12.0}/test/test_tvcom.py +0 -0
@@ -1,5 +1,6 @@
1
1
  stages:
2
2
  - test
3
+ - lint
3
4
  - deploy
4
5
 
5
6
  .jammy_image: &jammy
@@ -39,6 +40,17 @@ stages:
39
40
  coverage_format: cobertura
40
41
  path: coverage.xml
41
42
 
43
+ .lint_template: &lint
44
+ stage: lint
45
+ script:
46
+ - pylint --disable=missing-function-docstring
47
+ --disable=missing-class-docstring
48
+ --disable=missing-module-docstring
49
+ --disable=unknown-option-value
50
+ episoder/ test/ setup.py
51
+
52
+ - flake8 episoder/ test/ setup.py
53
+
42
54
  .deploy_setuptools_template: &deploy_setuptools
43
55
  stage: deploy
44
56
  script:
@@ -74,10 +86,12 @@ py310-jammy:test:
74
86
  <<: *test
75
87
 
76
88
  py310-jammy-setuptools:deploy:
89
+ needs: ["py310-jammy:test"]
77
90
  <<: *jammy
78
91
  <<: *deploy_setuptools
79
92
 
80
93
  py310-jammy-pip:deploy:
94
+ needs: ["py310-jammy:test"]
81
95
  <<: *jammy
82
96
  <<: *deploy_pip
83
97
 
@@ -85,13 +99,17 @@ py311-bookworm:test:
85
99
  <<: *bookworm
86
100
  <<: *test
87
101
 
102
+ py311-bookworm:lint:
103
+ <<: *bookworm
104
+ <<: *lint
105
+
88
106
  py311-bookworm-setuptools:deploy:
89
- needs: ["py311-bookworm:test"]
107
+ needs: ["py311-bookworm:lint"]
90
108
  <<: *bookworm
91
109
  <<: *deploy_setuptools
92
110
 
93
111
  py311-bookworm-pip:deploy:
94
- needs: ["py311-bookworm:test"]
112
+ needs: ["py311-bookworm:lint"]
95
113
  <<: *bookworm
96
114
  <<: *deploy_pip_venv
97
115
 
@@ -99,13 +117,17 @@ py312-noble:test:
99
117
  <<: *noble
100
118
  <<: *test
101
119
 
120
+ py312-noble:lint:
121
+ <<: *noble
122
+ <<: *lint
123
+
102
124
  py312-noble-setuptools:deploy:
103
- needs: ["py312-noble:test"]
125
+ needs: ["py312-noble:lint"]
104
126
  <<: *noble
105
127
  <<: *deploy_setuptools
106
128
 
107
129
  py312-noble-pip:deploy:
108
- needs: ["py312-noble:test"]
130
+ needs: ["py312-noble:lint"]
109
131
  <<: *noble
110
132
  <<: *deploy_pip_venv
111
133
 
@@ -113,12 +135,16 @@ py313-trixie:test:
113
135
  <<: *trixie
114
136
  <<: *test
115
137
 
138
+ py313-trixie:lint:
139
+ <<: *trixie
140
+ <<: *lint
141
+
116
142
  py313-trixie-setuptools:deploy:
117
- needs: ["py313-trixie:test"]
143
+ needs: ["py313-trixie:lint"]
118
144
  <<: *trixie
119
145
  <<: *deploy_setuptools
120
146
 
121
147
  py313-trixie-pip:deploy:
122
- needs: ["py313-trixie:test"]
148
+ needs: ["py313-trixie:lint"]
123
149
  <<: *trixie
124
150
  <<: *deploy_pip_venv
@@ -1,3 +1,9 @@
1
+ == 0.12.0 (17 Apr 2025) ==
2
+ * Add feature to test email configuration
3
+ * Fix typo in main script
4
+ * Fix dry-run mode for email notifications
5
+ * Drop --show parameter to notify command
6
+
1
7
  == 0.11.1 (8 Apr 2025) ==
2
8
  * Use Python's EmailMessage class when sending e-mail
3
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: episoder
3
- Version: 0.11.1
3
+ Version: 0.12.0
4
4
  Summary: TV episode notifier
5
5
  Home-page: https://code.ott.net/episoder/
6
6
  Author: Stefan Ott
@@ -21,6 +21,9 @@ Requires-Dist: argparse
21
21
  Requires-Dist: requests
22
22
  Requires-Dist: beautifulsoup4
23
23
  Requires-Dist: sqlalchemy>=1.3
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest; extra == "test"
26
+ Requires-Dist: pyfakefs; extra == "test"
24
27
 
25
28
  # episoder
26
29
 
@@ -0,0 +1,102 @@
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 logging import getLogger
19
+ from os import environ, path
20
+
21
+ from typing import BinaryIO, Optional
22
+
23
+ from .version import __version__
24
+
25
+
26
+ class InvalidDataFormatError(Exception):
27
+ def __init__(self, filename: str) -> None:
28
+ super().__init__(f'Please remove old/invalid data file: {filename}')
29
+
30
+
31
+ class EpisoderConfig:
32
+ # pylint: disable=too-few-public-methods,too-many-instance-attributes
33
+ def __init__(self) -> None:
34
+ self.agent = f'episoder/{__version__}'
35
+ self.datafile = path.join(environ['HOME'], '.episodes')
36
+ self.dateformat = '%a, %b %d, %Y'
37
+ self.format = '%airdate %show %seasonx%epnum'
38
+ self.tvdb_key = '8F15287C4B23B36E'
39
+
40
+ self.email_to: Optional[str] = None
41
+ self.email_username: Optional[str] = None
42
+ self.email_password: Optional[str] = None
43
+ self.email_server = 'localhost'
44
+ self.email_port = 587
45
+ self.email_tls = False
46
+
47
+
48
+ class ConfigLoader:
49
+ def __init__(self) -> None:
50
+ self._log = getLogger('Config')
51
+
52
+ def _load_rcfile(self, file_: BinaryIO) -> dict[str, str]:
53
+ def strip_comments(line: str) -> str:
54
+ return line.split('#')[0]
55
+
56
+ def valid(line: str) -> bool:
57
+ return '=' in line
58
+
59
+ self._log.info('Loading config file')
60
+ lines = file_.readlines()
61
+ decoded_lines = [x.decode('utf8').strip() for x in lines]
62
+ non_comments = map(strip_comments, decoded_lines)
63
+ valid_lines = filter(valid, non_comments)
64
+ return dict(line.split('=') for line in valid_lines)
65
+
66
+ def _update(self, cfg: EpisoderConfig, values: dict[str, str]) -> None:
67
+ cfg.agent = values.get('agent', cfg.agent)
68
+ cfg.datafile = values.get('data', cfg.datafile)
69
+ cfg.dateformat = values.get('dateformat', cfg.dateformat)
70
+ cfg.format = values.get('format', cfg.format)
71
+ cfg.tvdb_key = values.get('tvdb_key', cfg.tvdb_key)
72
+
73
+ cfg.email_to = values.get('email_to', cfg.email_to)
74
+ cfg.email_username = values.get('email_username', cfg.email_username)
75
+ cfg.email_password = values.get('email_password', cfg.email_password)
76
+ cfg.email_port = int(values.get('email_port', cfg.email_port))
77
+ cfg.email_server = values.get('email_server', cfg.email_server)
78
+ cfg.email_tls = bool(values.get('email_tls', cfg.email_tls))
79
+
80
+ def load(self, file: str) -> EpisoderConfig:
81
+ config = EpisoderConfig()
82
+
83
+ if path.exists(file):
84
+ with open(file, 'rb') as file_:
85
+ values = self._load_rcfile(file_)
86
+
87
+ self._log.debug('Settings loaded: %s', values)
88
+ self._update(config, values)
89
+ self._log.info("Loaded configuration")
90
+ else:
91
+ self._log.warning('No config file found, using defaults')
92
+
93
+ return config
94
+
95
+ def validate(self, config: EpisoderConfig) -> None:
96
+ filename = config.datafile
97
+ if path.exists(filename):
98
+ with open(filename, 'rb') as file:
99
+ data = file.read(6)
100
+
101
+ if data != b'SQLite':
102
+ raise InvalidDataFormatError(filename)
@@ -15,8 +15,6 @@
15
15
  # You should have received a copy of the GNU General Public License
16
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
 
18
- import logging
19
-
20
18
  from datetime import datetime
21
19
 
22
20
  from sqlalchemy import Column, Integer, Text, Date, ForeignKey, Sequence
@@ -63,12 +61,8 @@ class Show(Base):
63
61
 
64
62
  return self.name == other.name and self.url == other.url
65
63
 
66
- def remove_episodes_before(self, store, date):
67
-
68
- logging.debug("Removing episodes from before %s", date)
69
- store.remove_before(date, show=self)
70
-
71
64
 
65
+ # pylint: disable=too-many-instance-attributes
72
66
  class Episode(Base):
73
67
 
74
68
  __tablename__ = "episodes"
@@ -106,11 +100,11 @@ class Episode(Base):
106
100
 
107
101
  def __repr__(self):
108
102
 
109
- d = self.airdate
103
+ airdate = self.airdate
110
104
 
111
105
  return f'Episode("{self.title}", {self.season}, {self.episode}, ' \
112
- f'date({d.year}, {d.month}, {d.day}), "{self.prodnum}", ' \
113
- f'{self.totalnum})'
106
+ f'date({airdate.year}, {airdate.month}, {airdate.day}), ' \
107
+ f'"{self.prodnum}", {self.totalnum})'
114
108
 
115
109
  def __eq__(self, other):
116
110
 
@@ -17,6 +17,7 @@
17
17
 
18
18
  import logging
19
19
  from datetime import date, timedelta
20
+ from typing import Optional
20
21
 
21
22
  import sqlite3
22
23
  from sqlalchemy import create_engine, or_, and_, select, inspect
@@ -26,25 +27,20 @@ from .database import Episode, Show, Meta
26
27
 
27
28
 
28
29
  class Database:
29
-
30
- def __init__(self, path):
31
-
30
+ def __init__(self, path: str) -> None:
32
31
  self._path = path
33
- self.logger = logging.getLogger("Database")
32
+ self.logger = logging.getLogger('Database')
34
33
 
35
34
  self.open()
36
35
  self._initdb()
37
36
 
38
- def __str__(self):
39
-
40
- return f"Episoder Database at {self._path}"
41
-
42
- def __repr__(self):
43
-
44
- return f"Database({self._path})"
37
+ def __str__(self) -> str:
38
+ return f'Episoder Database at {self._path}'
45
39
 
46
- def _initdb(self):
40
+ def __repr__(self) -> str:
41
+ return f'Database({self._path})'
47
42
 
43
+ def _initdb(self) -> None:
48
44
  inspector = inspect(self.engine)
49
45
 
50
46
  # Initialize the database if all tables are missing
@@ -57,34 +53,30 @@ class Database:
57
53
  Meta.__table__.create(bind=self.engine)
58
54
  self.set_schema_version(4)
59
55
 
60
- def open(self):
61
-
62
- if self._path.find("://") > -1:
56
+ def open(self) -> None:
57
+ if self._path.find('://') > -1:
63
58
  self.engine = create_engine(self._path)
64
59
  else:
65
- self.engine = create_engine(f"sqlite:///{self._path}")
60
+ self.engine = create_engine(f'sqlite:///{self._path}')
66
61
 
67
- sm = sessionmaker(self.engine)
68
- self.session = sm()
62
+ session = sessionmaker(self.engine)
63
+ self.session = session()
69
64
  self.session.begin()
70
65
 
71
- def close(self):
72
-
66
+ def close(self) -> None:
73
67
  self.session.commit()
74
68
  self.session.close()
75
69
  self.engine.dispose()
76
70
 
77
- def set_schema_version(self, version):
78
-
71
+ def set_schema_version(self, version: int) -> None:
79
72
  meta = Meta()
80
- meta.key = "schema"
81
- meta.value = f"{version}"
73
+ meta.key = 'schema'
74
+ meta.value = f'{version}'
82
75
 
83
76
  with self.session.begin_nested():
84
77
  self.session.merge(meta)
85
78
 
86
- def get_schema_version(self):
87
-
79
+ def get_schema_version(self) -> int:
88
80
  inspector = inspect(self.engine)
89
81
  if not inspector.has_table(Meta.__table__.name):
90
82
  return 1
@@ -96,8 +88,7 @@ class Database:
96
88
 
97
89
  return 0
98
90
 
99
- def clear(self):
100
-
91
+ def clear(self) -> None:
101
92
  episodes = self.session.query(Episode).all()
102
93
 
103
94
  for episode in episodes:
@@ -105,18 +96,15 @@ class Database:
105
96
 
106
97
  self.session.flush()
107
98
 
108
- def migrate(self):
109
-
99
+ def migrate(self) -> None:
110
100
  schema_version = self.get_schema_version()
111
101
  self.logger.debug("Found schema version %s", schema_version)
112
102
 
113
103
  if schema_version < 0:
114
-
115
104
  self.logger.debug("Automatic schema updates disabled")
116
105
  return
117
106
 
118
107
  if schema_version == 1:
119
-
120
108
  # Upgrades from version 1 are rather harsh, we
121
109
  # simply drop and re-create the tables
122
110
  self.logger.debug("Upgrading to schema version 2")
@@ -134,7 +122,6 @@ class Database:
134
122
  self.set_schema_version(schema_version)
135
123
 
136
124
  if schema_version == 2:
137
-
138
125
  # Add two new columns to the shows table
139
126
  self.logger.debug("Upgrading to schema version 3")
140
127
 
@@ -155,7 +142,6 @@ class Database:
155
142
  self.set_schema_version(schema_version)
156
143
 
157
144
  if schema_version == 3:
158
-
159
145
  # Add a new column to the episodes table
160
146
  self.logger.debug("Upgrading to schema version 4")
161
147
 
@@ -173,8 +159,7 @@ class Database:
173
159
  schema_version = 4
174
160
  self.set_schema_version(schema_version)
175
161
 
176
- def get_expired_shows(self, today=date.today()):
177
-
162
+ def get_expired_shows(self, today: date = date.today()) -> list[Show]:
178
163
  delta_running = timedelta(2) # 2 days
179
164
  delta_suspended = timedelta(7) # 1 week
180
165
  delta_ended = timedelta(14) # 2 weeks
@@ -199,13 +184,11 @@ class Database:
199
184
 
200
185
  return shows.all()
201
186
 
202
- def get_enabled_shows(self):
203
-
187
+ def get_enabled_shows(self) -> list[Show]:
204
188
  shows = self.session.query(Show).filter(Show.enabled)
205
189
  return shows.all()
206
190
 
207
- def get_show_by_url(self, url):
208
-
191
+ def get_show_by_url(self, url: str) -> Optional[Show]:
209
192
  shows = self.session.query(Show).filter(Show.url == url)
210
193
 
211
194
  if shows.count() < 1:
@@ -213,26 +196,17 @@ class Database:
213
196
 
214
197
  return shows.first()
215
198
 
216
- def get_show_by_id(self, show_id):
217
-
199
+ def get_show_by_id(self, show_id: int) -> Optional[Show]:
218
200
  return self.session.scalars(
219
201
  select(Show).where(Show.id == show_id)).first()
220
202
 
221
- def add_show(self, show):
222
-
203
+ def add_show(self, show: Show) -> Show:
223
204
  show = self.session.merge(show)
224
205
  self.session.flush()
225
206
  return show
226
207
 
227
- def remove_show(self, show_id):
228
-
229
- show = self.session.scalars(
230
- select(Show).where(Show.id == show_id)).first()
231
-
232
- if not show:
233
- self.logger.error("No such show")
234
- return
235
-
208
+ def remove_show(self, show: Show) -> None:
209
+ assert show is not None
236
210
  episodes = self.session.scalars(select(Episode))
237
211
 
238
212
  hits = [e for e in episodes if e.show_id == show.id]
@@ -242,51 +216,46 @@ class Database:
242
216
  self.session.delete(show)
243
217
  self.session.flush()
244
218
 
245
- def get_shows(self):
246
-
219
+ def get_shows(self) -> list[Show]:
247
220
  return self.session.query(Show).all()
248
221
 
249
- def add_episode(self, episode, show):
250
-
222
+ def add_episode(self, episode: Episode, show: Show) -> None:
251
223
  episode.show_id = show.id
252
224
  self.session.merge(episode)
253
225
  self.session.flush()
254
226
 
255
- def get_episodes(self, basedate=date.today(), n_days=0):
256
-
257
- enddate = basedate + timedelta(n_days)
227
+ def get_episodes(self, basedate: date = date.today(),
228
+ days: int = 0) -> list[Episode]:
229
+ enddate = basedate + timedelta(days)
258
230
 
259
231
  return self.session.query(Episode). \
260
232
  filter(Episode.airdate >= basedate). \
261
233
  filter(Episode.airdate <= enddate). \
262
234
  order_by(Episode.airdate).all()
263
235
 
264
- def search(self, search):
265
-
266
- q = self.session.query(Episode).join(Episode.show)
267
- return q.filter(or_(
268
- Episode.title.like(f"%%{search}%%"),
269
- Show.name.like(f"%%{search}%%"))). \
236
+ def search(self, term: str) -> list[Episode]:
237
+ query = self.session.query(Episode).join(Episode.show)
238
+ return query.filter(or_(
239
+ Episode.title.like(f"%%{term}%%"),
240
+ Show.name.like(f"%%{term}%%"))). \
270
241
  order_by(Episode.airdate.asc()).all()
271
242
 
272
- def commit(self):
273
-
243
+ def commit(self) -> None:
274
244
  self.session.commit()
275
245
  self.session.begin()
276
246
 
277
- def rollback(self):
278
-
247
+ def rollback(self) -> None:
279
248
  self.session.rollback()
280
249
  self.session.begin()
281
250
 
282
- def remove_before(self, then, show=None):
283
-
284
- eps = self.session.query(Episode).filter(Episode.airdate < then)
251
+ def remove_before(self, then: date, show: Optional[Show] = None) -> None:
252
+ logging.debug("Removing episodes from before %s", then)
253
+ episodes = self.session.query(Episode).filter(Episode.airdate < then)
285
254
 
286
255
  if show:
287
- eps = eps.filter(Episode.show == show)
256
+ episodes = episodes.filter(Episode.show == show)
288
257
 
289
- for episode in eps:
258
+ for episode in episodes:
290
259
  self.session.delete(episode)
291
260
 
292
261
  self.commit()