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.
- {episoder-0.11.1 → episoder-0.12.0}/.gitlab-ci.yml +32 -6
- {episoder-0.11.1 → episoder-0.12.0}/CHANGELOG +6 -0
- {episoder-0.11.1 → episoder-0.12.0}/PKG-INFO +4 -1
- episoder-0.12.0/episoder/config.py +102 -0
- {episoder-0.11.1 → episoder-0.12.0}/episoder/database.py +4 -10
- {episoder-0.11.1 → episoder-0.12.0}/episoder/episoder.py +44 -75
- episoder-0.12.0/episoder/main.py +377 -0
- episoder-0.12.0/episoder/options.py +176 -0
- episoder-0.12.0/episoder/output.py +153 -0
- {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/epguides.py +9 -7
- {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/parser.py +3 -2
- {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/thetvdb.py +8 -7
- {episoder-0.11.1 → episoder-0.12.0}/episoder/version.py +2 -2
- {episoder-0.11.1 → episoder-0.12.0}/episoder.1 +5 -2
- {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/PKG-INFO +4 -1
- {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/SOURCES.txt +6 -0
- {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/requires.txt +4 -0
- {episoder-0.11.1 → episoder-0.12.0}/setup.cfg +5 -0
- episoder-0.12.0/test/test_commands.py +220 -0
- episoder-0.12.0/test/test_config.py +135 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/test_database.py +140 -139
- {episoder-0.11.1 → episoder-0.12.0}/test/test_epguides_parser.py +113 -108
- {episoder-0.11.1 → episoder-0.12.0}/test/test_episode.py +12 -12
- episoder-0.12.0/test/test_main.py +911 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/test_migration.py +22 -22
- episoder-0.12.0/test/test_notify.py +129 -0
- episoder-0.12.0/test/test_options.py +427 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/test_output.py +61 -61
- {episoder-0.11.1 → episoder-0.12.0}/test/test_show.py +12 -12
- {episoder-0.11.1 → episoder-0.12.0}/test/test_tvdb.py +29 -29
- {episoder-0.11.1 → episoder-0.12.0}/test/util.py +3 -9
- episoder-0.11.1/episoder/main.py +0 -449
- episoder-0.11.1/episoder/output.py +0 -169
- episoder-0.11.1/test/test_notify.py +0 -265
- {episoder-0.11.1 → episoder-0.12.0}/.gitignore +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/COPYING +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/README.md +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/episoder/__init__.py +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/__init__.py +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/episoder/sources/tvcom.py +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/dependency_links.txt +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/entry_points.txt +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/episoder.egg-info/top_level.txt +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/examples/home.episoder +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/pyproject.toml +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/setup.py +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/__init__.py +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_48_hours_mistery.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_american_idol.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_bsg.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_buzzcocks.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_doctor_who.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_eureka.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_futurama.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_house.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_kr2008.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_lost.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_lost_html.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_lower_decks.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_midsomer_murders.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_picard.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/epguides_test_iso_8859_1.html +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_error_404.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_search_frasier.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_search_friends.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_260.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_260_1_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_261.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_261_1_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_261_2_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_261_3_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_262.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_262_1_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_263.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_268156.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_268156_1_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_295640.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_295640_1_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_295640_2_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_73739.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_73739_1_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_75397.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/fixtures/tvdb_test_show_75397_1_eps.json +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/test_metainfo.py +0 -0
- {episoder-0.11.1 → episoder-0.12.0}/test/test_sources.py +0 -0
- {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:
|
|
107
|
+
needs: ["py311-bookworm:lint"]
|
|
90
108
|
<<: *bookworm
|
|
91
109
|
<<: *deploy_setuptools
|
|
92
110
|
|
|
93
111
|
py311-bookworm-pip:deploy:
|
|
94
|
-
needs: ["py311-bookworm:
|
|
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:
|
|
125
|
+
needs: ["py312-noble:lint"]
|
|
104
126
|
<<: *noble
|
|
105
127
|
<<: *deploy_setuptools
|
|
106
128
|
|
|
107
129
|
py312-noble-pip:deploy:
|
|
108
|
-
needs: ["py312-noble:
|
|
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:
|
|
143
|
+
needs: ["py313-trixie:lint"]
|
|
118
144
|
<<: *trixie
|
|
119
145
|
<<: *deploy_setuptools
|
|
120
146
|
|
|
121
147
|
py313-trixie-pip:deploy:
|
|
122
|
-
needs: ["py313-trixie:
|
|
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.
|
|
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
|
-
|
|
103
|
+
airdate = self.airdate
|
|
110
104
|
|
|
111
105
|
return f'Episode("{self.title}", {self.season}, {self.episode}, ' \
|
|
112
|
-
f'date({
|
|
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(
|
|
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
|
|
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
|
|
60
|
+
self.engine = create_engine(f'sqlite:///{self._path}')
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
self.session =
|
|
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 =
|
|
81
|
-
meta.value = f
|
|
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,
|
|
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(),
|
|
256
|
-
|
|
257
|
-
enddate = basedate + timedelta(
|
|
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,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
+
episodes = episodes.filter(Episode.show == show)
|
|
288
257
|
|
|
289
|
-
for episode in
|
|
258
|
+
for episode in episodes:
|
|
290
259
|
self.session.delete(episode)
|
|
291
260
|
|
|
292
261
|
self.commit()
|