episoder 0.12.0__tar.gz → 1.1.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-1.1.0/MANIFEST.in +2 -0
- episoder-1.1.0/PKG-INFO +115 -0
- episoder-1.1.0/README.md +79 -0
- episoder-1.1.0/episoder/cache.py +49 -0
- {episoder-0.12.0 → episoder-1.1.0}/episoder/config.py +2 -2
- {episoder-0.12.0 → episoder-1.1.0}/episoder/database.py +59 -55
- {episoder-0.12.0 → episoder-1.1.0}/episoder/episoder.py +17 -3
- episoder-1.1.0/episoder/errors.py +9 -0
- {episoder-0.12.0 → episoder-1.1.0}/episoder/main.py +17 -14
- {episoder-0.12.0 → episoder-1.1.0}/episoder/options.py +1 -1
- {episoder-0.12.0 → episoder-1.1.0}/episoder/output.py +1 -1
- {episoder-0.12.0 → episoder-1.1.0}/episoder/sources/__init__.py +7 -4
- {episoder-0.12.0 → episoder-1.1.0}/episoder/sources/epguides.py +2 -2
- {episoder-0.12.0 → episoder-1.1.0}/episoder/sources/parser.py +1 -1
- episoder-1.1.0/episoder/sources/thetvdb.py +20 -0
- episoder-0.12.0/episoder/sources/thetvdb.py → episoder-1.1.0/episoder/sources/thetvdb_legacy.py +43 -41
- episoder-1.1.0/episoder/sources/thetvdb_v4.py +325 -0
- {episoder-0.12.0 → episoder-1.1.0}/episoder/sources/tvcom.py +1 -1
- {episoder-0.12.0 → episoder-1.1.0}/episoder/version.py +2 -2
- {episoder-0.12.0 → episoder-1.1.0}/episoder.1 +13 -3
- episoder-1.1.0/episoder.egg-info/PKG-INFO +115 -0
- episoder-1.1.0/episoder.egg-info/SOURCES.txt +47 -0
- episoder-1.1.0/episoder.egg-info/requires.txt +17 -0
- {episoder-0.12.0 → episoder-1.1.0}/setup.cfg +11 -3
- episoder-1.1.0/test/test_cache.py +79 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_config.py +1 -1
- {episoder-0.12.0 → episoder-1.1.0}/test/test_main.py +8 -15
- {episoder-0.12.0 → episoder-1.1.0}/test/test_sources.py +36 -9
- episoder-0.12.0/test/test_tvdb.py → episoder-1.1.0/test/test_tvdb_legacy.py +14 -13
- episoder-1.1.0/test/test_tvdb_v4.py +544 -0
- episoder-0.12.0/.gitignore +0 -2
- episoder-0.12.0/.gitlab-ci.yml +0 -150
- episoder-0.12.0/CHANGELOG +0 -228
- episoder-0.12.0/PKG-INFO +0 -74
- episoder-0.12.0/README.md +0 -47
- episoder-0.12.0/episoder.egg-info/PKG-INFO +0 -74
- episoder-0.12.0/episoder.egg-info/SOURCES.txt +0 -82
- episoder-0.12.0/episoder.egg-info/requires.txt +0 -8
- episoder-0.12.0/setup.py +0 -6
- episoder-0.12.0/test/__init__.py +0 -0
- episoder-0.12.0/test/fixtures/epguides_48_hours_mistery.html +0 -399
- episoder-0.12.0/test/fixtures/epguides_american_idol.html +0 -872
- episoder-0.12.0/test/fixtures/epguides_bsg.html +0 -538
- episoder-0.12.0/test/fixtures/epguides_buzzcocks.html +0 -464
- episoder-0.12.0/test/fixtures/epguides_doctor_who.html +0 -842
- episoder-0.12.0/test/fixtures/epguides_eureka.html +0 -362
- episoder-0.12.0/test/fixtures/epguides_futurama.html +0 -407
- episoder-0.12.0/test/fixtures/epguides_house.html +0 -490
- episoder-0.12.0/test/fixtures/epguides_kr2008.html +0 -256
- episoder-0.12.0/test/fixtures/epguides_lost.html +0 -478
- episoder-0.12.0/test/fixtures/epguides_lost_html.html +0 -602
- episoder-0.12.0/test/fixtures/epguides_lower_decks.html +0 -340
- episoder-0.12.0/test/fixtures/epguides_midsomer_murders.html +0 -398
- episoder-0.12.0/test/fixtures/epguides_picard.html +0 -380
- episoder-0.12.0/test/fixtures/epguides_test_iso_8859_1.html +0 -138
- episoder-0.12.0/test/fixtures/tvdb_error_404.json +0 -3
- episoder-0.12.0/test/fixtures/tvdb_search_frasier.json +0 -10
- episoder-0.12.0/test/fixtures/tvdb_search_friends.json +0 -28
- episoder-0.12.0/test/fixtures/tvdb_test_show_260.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_260_1_eps.json +0 -19
- episoder-0.12.0/test/fixtures/tvdb_test_show_261.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_261_1_eps.json +0 -23
- episoder-0.12.0/test/fixtures/tvdb_test_show_261_2_eps.json +0 -23
- episoder-0.12.0/test/fixtures/tvdb_test_show_261_3_eps.json +0 -18
- episoder-0.12.0/test/fixtures/tvdb_test_show_262.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_262_1_eps.json +0 -20
- episoder-0.12.0/test/fixtures/tvdb_test_show_263.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_268156.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_268156_1_eps.json +0 -520
- episoder-0.12.0/test/fixtures/tvdb_test_show_295640.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_295640_1_eps.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_295640_2_eps.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_73739.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_73739_1_eps.json +0 -14
- episoder-0.12.0/test/fixtures/tvdb_test_show_75397.json +0 -1
- episoder-0.12.0/test/fixtures/tvdb_test_show_75397_1_eps.json +0 -1
- episoder-0.12.0/test/util.py +0 -54
- {episoder-0.12.0 → episoder-1.1.0}/COPYING +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/episoder/__init__.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/episoder.egg-info/dependency_links.txt +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/episoder.egg-info/entry_points.txt +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/episoder.egg-info/top_level.txt +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/examples/home.episoder +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/pyproject.toml +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_commands.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_database.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_epguides_parser.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_episode.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_metainfo.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_migration.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_notify.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_options.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_output.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_show.py +0 -0
- {episoder-0.12.0 → episoder-1.1.0}/test/test_tvcom.py +0 -0
episoder-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: episoder
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: TV episode notifier
|
|
5
|
+
Home-page: https://code.ott.net/episoder/
|
|
6
|
+
Author: Stefan Ott
|
|
7
|
+
Author-email: stefan@ott.net
|
|
8
|
+
License: GPL-3.0-or-later
|
|
9
|
+
Project-URL: Source, https://git.ott.net/episoder/
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Topic :: Multimedia :: Video
|
|
17
|
+
Requires-Python: >=3.6
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: COPYING
|
|
20
|
+
Requires-Dist: argparse
|
|
21
|
+
Requires-Dist: beautifulsoup4
|
|
22
|
+
Requires-Dist: platformdirs
|
|
23
|
+
Requires-Dist: requests
|
|
24
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: mypy; extra == "test"
|
|
27
|
+
Requires-Dist: flake8; extra == "test"
|
|
28
|
+
Requires-Dist: pytest; extra == "test"
|
|
29
|
+
Requires-Dist: pyfakefs; extra == "test"
|
|
30
|
+
Requires-Dist: types-requests; extra == "test"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build; extra == "dev"
|
|
33
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
34
|
+
Requires-Dist: twine; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# episoder
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
Episoder is a command-line tool that tells you about new episodes of your
|
|
42
|
+
favourite TV shows. It gets its data from online TV episode guides such as
|
|
43
|
+
epguides.com and TheTVDB.
|
|
44
|
+
|
|
45
|
+
There is also a web frontend for episoder, called
|
|
46
|
+
[webisoder](https://code.ott.net/webisoder).
|
|
47
|
+
|
|
48
|
+
## Dependencies
|
|
49
|
+
|
|
50
|
+
In order to use episoder, you need to have Python installed on your system. In
|
|
51
|
+
addition to the default distribution, episoder uses the `requests`, `argparse`,
|
|
52
|
+
`sqlalchemy` and `beautifulsoup4` packages.
|
|
53
|
+
|
|
54
|
+
## Downloading
|
|
55
|
+
|
|
56
|
+
The official website, bug tracker etc. for episoder can be found at
|
|
57
|
+
[code.ott.net/episoder](https://code.ott.net/episoder/).
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
You can install episoder from PyPI by running `pip install episoder` or you can
|
|
62
|
+
download the tarball from the official website and install it manually using
|
|
63
|
+
`pip install episoder-<version>.tar.gz`.
|
|
64
|
+
|
|
65
|
+
## Getting started
|
|
66
|
+
|
|
67
|
+
First, find a show you're interested in:
|
|
68
|
+
|
|
69
|
+
$ episoder search firefly
|
|
70
|
+
ID Name
|
|
71
|
+
------- --------------------
|
|
72
|
+
78874 Firefly
|
|
73
|
+
384248 Firefly Lane
|
|
74
|
+
329088 Firefly (2017)
|
|
75
|
+
|
|
76
|
+
Add that show to episoder:
|
|
77
|
+
|
|
78
|
+
$ episoder add 78874
|
|
79
|
+
|
|
80
|
+
Update your database:
|
|
81
|
+
|
|
82
|
+
$ episoder update
|
|
83
|
+
|
|
84
|
+
And finally show your episodes:
|
|
85
|
+
|
|
86
|
+
$ episoder list
|
|
87
|
+
|
|
88
|
+
The list may be empty, by default episoder deletes old episodes and will only
|
|
89
|
+
show you the next few days. The `--nodate` option to the `update` and `list`
|
|
90
|
+
command will change that, in case you care about days long gone and/or far in
|
|
91
|
+
the future.
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
Episoder does not need any configuration, it should run fine out of the box.
|
|
96
|
+
Still, some of its behaviour can be tweaked through a config file at
|
|
97
|
+
`~/.episoder`, the [man page](https://code.ott.net/episoder/doc/config.html) has
|
|
98
|
+
all the details.
|
|
99
|
+
|
|
100
|
+
## Using episoder
|
|
101
|
+
|
|
102
|
+
To use episoder you will typically:
|
|
103
|
+
|
|
104
|
+
* Create a cron job to update your database once a day using `episoder update`
|
|
105
|
+
* Add `episoder list` to your `~/.bashrc`, `~/.bash_profile`, `/etc/bash.bashrc`
|
|
106
|
+
or `/etc/profile` if you want to see your upcoming shows every time you start
|
|
107
|
+
a shell. Consult your shell's documentation to find out which file to use.
|
|
108
|
+
* Or run `episoder notify` at regular intervals to get notifications by e-mail.
|
|
109
|
+
|
|
110
|
+
## More documentation
|
|
111
|
+
|
|
112
|
+
There is some documentation to help you get started:
|
|
113
|
+
|
|
114
|
+
* [Using episoder](https://code.ott.net/episoder/doc/use.html)
|
|
115
|
+
* [Episoder configuration](https://code.ott.net/episoder/doc/config.html)
|
episoder-1.1.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# episoder
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Episoder is a command-line tool that tells you about new episodes of your
|
|
6
|
+
favourite TV shows. It gets its data from online TV episode guides such as
|
|
7
|
+
epguides.com and TheTVDB.
|
|
8
|
+
|
|
9
|
+
There is also a web frontend for episoder, called
|
|
10
|
+
[webisoder](https://code.ott.net/webisoder).
|
|
11
|
+
|
|
12
|
+
## Dependencies
|
|
13
|
+
|
|
14
|
+
In order to use episoder, you need to have Python installed on your system. In
|
|
15
|
+
addition to the default distribution, episoder uses the `requests`, `argparse`,
|
|
16
|
+
`sqlalchemy` and `beautifulsoup4` packages.
|
|
17
|
+
|
|
18
|
+
## Downloading
|
|
19
|
+
|
|
20
|
+
The official website, bug tracker etc. for episoder can be found at
|
|
21
|
+
[code.ott.net/episoder](https://code.ott.net/episoder/).
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
You can install episoder from PyPI by running `pip install episoder` or you can
|
|
26
|
+
download the tarball from the official website and install it manually using
|
|
27
|
+
`pip install episoder-<version>.tar.gz`.
|
|
28
|
+
|
|
29
|
+
## Getting started
|
|
30
|
+
|
|
31
|
+
First, find a show you're interested in:
|
|
32
|
+
|
|
33
|
+
$ episoder search firefly
|
|
34
|
+
ID Name
|
|
35
|
+
------- --------------------
|
|
36
|
+
78874 Firefly
|
|
37
|
+
384248 Firefly Lane
|
|
38
|
+
329088 Firefly (2017)
|
|
39
|
+
|
|
40
|
+
Add that show to episoder:
|
|
41
|
+
|
|
42
|
+
$ episoder add 78874
|
|
43
|
+
|
|
44
|
+
Update your database:
|
|
45
|
+
|
|
46
|
+
$ episoder update
|
|
47
|
+
|
|
48
|
+
And finally show your episodes:
|
|
49
|
+
|
|
50
|
+
$ episoder list
|
|
51
|
+
|
|
52
|
+
The list may be empty, by default episoder deletes old episodes and will only
|
|
53
|
+
show you the next few days. The `--nodate` option to the `update` and `list`
|
|
54
|
+
command will change that, in case you care about days long gone and/or far in
|
|
55
|
+
the future.
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
Episoder does not need any configuration, it should run fine out of the box.
|
|
60
|
+
Still, some of its behaviour can be tweaked through a config file at
|
|
61
|
+
`~/.episoder`, the [man page](https://code.ott.net/episoder/doc/config.html) has
|
|
62
|
+
all the details.
|
|
63
|
+
|
|
64
|
+
## Using episoder
|
|
65
|
+
|
|
66
|
+
To use episoder you will typically:
|
|
67
|
+
|
|
68
|
+
* Create a cron job to update your database once a day using `episoder update`
|
|
69
|
+
* Add `episoder list` to your `~/.bashrc`, `~/.bash_profile`, `/etc/bash.bashrc`
|
|
70
|
+
or `/etc/profile` if you want to see your upcoming shows every time you start
|
|
71
|
+
a shell. Consult your shell's documentation to find out which file to use.
|
|
72
|
+
* Or run `episoder notify` at regular intervals to get notifications by e-mail.
|
|
73
|
+
|
|
74
|
+
## More documentation
|
|
75
|
+
|
|
76
|
+
There is some documentation to help you get started:
|
|
77
|
+
|
|
78
|
+
* [Using episoder](https://code.ott.net/episoder/doc/use.html)
|
|
79
|
+
* [Episoder configuration](https://code.ott.net/episoder/doc/config.html)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# episoder, https://code.ott.net/episoder
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2004-2026 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
|
+
import os
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
_FILE_NAME_TVDB_TOKEN = "tvdb_token"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Cache:
|
|
25
|
+
def __init__(self, path: str) -> None:
|
|
26
|
+
self._path = path
|
|
27
|
+
|
|
28
|
+
def set_tvdb_token(self, token: str) -> None:
|
|
29
|
+
if not os.path.exists(self._path):
|
|
30
|
+
os.mkdir(self._path)
|
|
31
|
+
|
|
32
|
+
fn = os.path.join(self._path, _FILE_NAME_TVDB_TOKEN)
|
|
33
|
+
with open(fn, 'w', encoding='utf8') as file:
|
|
34
|
+
file.write(token)
|
|
35
|
+
|
|
36
|
+
def get_tvdb_token(self) -> Optional[str]:
|
|
37
|
+
fn = os.path.join(self._path, _FILE_NAME_TVDB_TOKEN)
|
|
38
|
+
if not os.path.exists(fn):
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
with open(fn, 'r', encoding='utf8') as file:
|
|
42
|
+
token = file.read()
|
|
43
|
+
|
|
44
|
+
return token
|
|
45
|
+
|
|
46
|
+
def clear_tvdb_token(self) -> None:
|
|
47
|
+
fn = os.path.join(self._path, _FILE_NAME_TVDB_TOKEN)
|
|
48
|
+
if os.path.exists(fn):
|
|
49
|
+
os.unlink(fn)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# episoder, https://code.ott.net/episoder
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2004-
|
|
3
|
+
# Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -35,7 +35,7 @@ class EpisoderConfig:
|
|
|
35
35
|
self.datafile = path.join(environ['HOME'], '.episodes')
|
|
36
36
|
self.dateformat = '%a, %b %d, %Y'
|
|
37
37
|
self.format = '%airdate %show %seasonx%epnum'
|
|
38
|
-
self.tvdb_key = '
|
|
38
|
+
self.tvdb_key = '1f4c52fc-410a-4868-9724-4c3eba19bdc7'
|
|
39
39
|
|
|
40
40
|
self.email_to: Optional[str] = None
|
|
41
41
|
self.email_username: Optional[str] = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# episoder, https://code.ott.net/episoder
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2004-
|
|
3
|
+
# Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -15,98 +15,105 @@
|
|
|
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
|
-
from datetime import datetime
|
|
18
|
+
from datetime import date, datetime
|
|
19
|
+
from typing import Any, Optional
|
|
19
20
|
|
|
20
|
-
from sqlalchemy import
|
|
21
|
-
from sqlalchemy import
|
|
22
|
-
from sqlalchemy.orm import backref,
|
|
21
|
+
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, Text
|
|
22
|
+
from sqlalchemy import Sequence
|
|
23
|
+
from sqlalchemy.orm import backref, DeclarativeBase, relationship
|
|
24
|
+
from sqlalchemy.orm import mapped_column
|
|
23
25
|
|
|
24
|
-
Base = declarative_base()
|
|
25
26
|
|
|
27
|
+
# pylint: disable=too-few-public-methods
|
|
28
|
+
class Base(DeclarativeBase):
|
|
29
|
+
pass
|
|
26
30
|
|
|
27
|
-
class Show(Base):
|
|
28
31
|
|
|
32
|
+
class Show(Base):
|
|
29
33
|
RUNNING = 1
|
|
30
34
|
SUSPENDED = 2
|
|
31
35
|
ENDED = 3
|
|
32
36
|
|
|
33
37
|
__tablename__ = "shows"
|
|
34
38
|
|
|
35
|
-
id =
|
|
36
|
-
|
|
37
|
-
name =
|
|
38
|
-
url =
|
|
39
|
-
updated =
|
|
40
|
-
enabled =
|
|
41
|
-
status =
|
|
42
|
-
|
|
43
|
-
def __init__(self, name, url="",
|
|
44
|
-
|
|
39
|
+
id = mapped_column("show_id", Integer, Sequence("shows_show_id_seq"),
|
|
40
|
+
primary_key=True)
|
|
41
|
+
name = mapped_column("show_name", Text)
|
|
42
|
+
url = mapped_column(Text, unique=True)
|
|
43
|
+
updated = mapped_column(DateTime)
|
|
44
|
+
enabled = mapped_column(Boolean)
|
|
45
|
+
status = mapped_column(Integer, default=RUNNING)
|
|
46
|
+
|
|
47
|
+
def __init__(self, name: str, url: str = "",
|
|
48
|
+
updated: datetime = datetime.fromtimestamp(0)) -> None:
|
|
49
|
+
super().__init__()
|
|
45
50
|
self.name = name
|
|
46
51
|
self.url = url
|
|
47
52
|
self.updated = updated
|
|
48
53
|
self.status = Show.RUNNING
|
|
49
|
-
self.episodes = []
|
|
54
|
+
self.episodes: list[Episode] = []
|
|
50
55
|
self.enabled = True
|
|
51
56
|
|
|
52
|
-
def __str__(self):
|
|
53
|
-
|
|
57
|
+
def __str__(self) -> str:
|
|
54
58
|
return f"Show: {self.name}"
|
|
55
59
|
|
|
56
|
-
def __repr__(self):
|
|
57
|
-
|
|
60
|
+
def __repr__(self) -> str:
|
|
58
61
|
return f'Show("{self.name}", "{self.url}")'
|
|
59
62
|
|
|
60
|
-
def __eq__(self, other):
|
|
63
|
+
def __eq__(self, other: object) -> Any:
|
|
64
|
+
if not isinstance(other, Show):
|
|
65
|
+
return NotImplemented
|
|
61
66
|
|
|
62
67
|
return self.name == other.name and self.url == other.url
|
|
63
68
|
|
|
64
69
|
|
|
65
70
|
# pylint: disable=too-many-instance-attributes
|
|
66
71
|
class Episode(Base):
|
|
67
|
-
|
|
68
72
|
__tablename__ = "episodes"
|
|
69
73
|
|
|
70
|
-
show_id =
|
|
71
|
-
episode =
|
|
72
|
-
airdate =
|
|
73
|
-
season =
|
|
74
|
-
title =
|
|
75
|
-
totalnum =
|
|
76
|
-
prodnum =
|
|
77
|
-
notified =
|
|
74
|
+
show_id = mapped_column(Integer, ForeignKey(Show.id), primary_key=True)
|
|
75
|
+
episode = mapped_column("num", Integer, primary_key=True)
|
|
76
|
+
airdate = mapped_column(Date)
|
|
77
|
+
season = mapped_column(Integer, primary_key=True)
|
|
78
|
+
title = mapped_column(Text)
|
|
79
|
+
totalnum = mapped_column(Integer)
|
|
80
|
+
prodnum = mapped_column(Text)
|
|
81
|
+
notified = mapped_column(Date)
|
|
78
82
|
show = relationship(Show, backref=backref("episodes", cascade="all"))
|
|
79
83
|
|
|
80
84
|
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
81
|
-
def __init__(self, name, season, episode, aired
|
|
82
|
-
|
|
85
|
+
def __init__(self, name: str, season: int, episode: int, aired: date,
|
|
86
|
+
prodnum: Optional[str], total: int) -> None:
|
|
87
|
+
super().__init__()
|
|
83
88
|
self.title = name
|
|
84
|
-
self.season =
|
|
85
|
-
self.episode =
|
|
89
|
+
self.season = season
|
|
90
|
+
self.episode = episode
|
|
86
91
|
self.airdate = aired
|
|
87
92
|
self.prodnum = prodnum
|
|
88
|
-
self.totalnum =
|
|
89
|
-
|
|
90
|
-
def __lt__(self, other):
|
|
91
|
-
|
|
92
|
-
return ((self.season < other.season)
|
|
93
|
-
or ((self.season == other.season)
|
|
94
|
-
and (self.episode < other.episode)))
|
|
95
|
-
|
|
96
|
-
def __str__(self):
|
|
93
|
+
self.totalnum = total
|
|
97
94
|
|
|
95
|
+
def __str__(self) -> str:
|
|
98
96
|
return f"{self.show.name} {self.season}x{self.episode:02d}: " \
|
|
99
97
|
f"{self.title}"
|
|
100
98
|
|
|
101
|
-
def __repr__(self):
|
|
102
|
-
|
|
99
|
+
def __repr__(self) -> str:
|
|
103
100
|
airdate = self.airdate
|
|
104
101
|
|
|
105
102
|
return f'Episode("{self.title}", {self.season}, {self.episode}, ' \
|
|
106
103
|
f'date({airdate.year}, {airdate.month}, {airdate.day}), ' \
|
|
107
104
|
f'"{self.prodnum}", {self.totalnum})'
|
|
108
105
|
|
|
109
|
-
def
|
|
106
|
+
def __lt__(self, other: object) -> Any:
|
|
107
|
+
if not isinstance(other, Episode):
|
|
108
|
+
return NotImplemented
|
|
109
|
+
|
|
110
|
+
return ((self.season < other.season)
|
|
111
|
+
or ((self.season == other.season)
|
|
112
|
+
and (self.episode < other.episode)))
|
|
113
|
+
|
|
114
|
+
def __eq__(self, other: object) -> Any:
|
|
115
|
+
if not isinstance(other, Episode):
|
|
116
|
+
return NotImplemented
|
|
110
117
|
|
|
111
118
|
return (self.show_id == other.show_id
|
|
112
119
|
and self.season == other.season
|
|
@@ -114,15 +121,12 @@ class Episode(Base):
|
|
|
114
121
|
|
|
115
122
|
|
|
116
123
|
class Meta(Base):
|
|
117
|
-
|
|
118
124
|
__tablename__ = "meta"
|
|
119
|
-
key =
|
|
120
|
-
value =
|
|
121
|
-
|
|
122
|
-
def __str__(self):
|
|
125
|
+
key = mapped_column(Text, primary_key=True)
|
|
126
|
+
value = mapped_column(Text)
|
|
123
127
|
|
|
128
|
+
def __str__(self) -> str:
|
|
124
129
|
return f"Meta: {self.key} = {self.value}"
|
|
125
130
|
|
|
126
|
-
def __repr__(self):
|
|
127
|
-
|
|
131
|
+
def __repr__(self) -> str:
|
|
128
132
|
return str(self)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# episoder, https://code.ott.net/episoder
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2004-
|
|
3
|
+
# Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -20,7 +20,7 @@ from datetime import date, timedelta
|
|
|
20
20
|
from typing import Optional
|
|
21
21
|
|
|
22
22
|
import sqlite3
|
|
23
|
-
from sqlalchemy import create_engine, or_, and_, select, inspect
|
|
23
|
+
from sqlalchemy import create_engine, or_, and_, select, inspect, Table
|
|
24
24
|
from sqlalchemy.orm import sessionmaker
|
|
25
25
|
|
|
26
26
|
from .database import Episode, Show, Meta
|
|
@@ -45,12 +45,19 @@ class Database:
|
|
|
45
45
|
|
|
46
46
|
# Initialize the database if all tables are missing
|
|
47
47
|
names = [Show, Episode, Meta]
|
|
48
|
-
tables = [t for t in names
|
|
48
|
+
tables = [t for t in names
|
|
49
|
+
if inspector.has_table(t.__table__.name)] # type: ignore
|
|
49
50
|
|
|
50
51
|
if len(tables) < 1:
|
|
52
|
+
assert isinstance(Show.__table__, Table)
|
|
51
53
|
Show.__table__.create(bind=self.engine)
|
|
54
|
+
|
|
55
|
+
assert isinstance(Episode.__table__, Table)
|
|
52
56
|
Episode.__table__.create(bind=self.engine)
|
|
57
|
+
|
|
58
|
+
assert isinstance(Meta.__table__, Table)
|
|
53
59
|
Meta.__table__.create(bind=self.engine)
|
|
60
|
+
|
|
54
61
|
self.set_schema_version(4)
|
|
55
62
|
|
|
56
63
|
def open(self) -> None:
|
|
@@ -78,6 +85,8 @@ class Database:
|
|
|
78
85
|
|
|
79
86
|
def get_schema_version(self) -> int:
|
|
80
87
|
inspector = inspect(self.engine)
|
|
88
|
+
|
|
89
|
+
assert isinstance(Meta.__table__, Table)
|
|
81
90
|
if not inspector.has_table(Meta.__table__.name):
|
|
82
91
|
return 1
|
|
83
92
|
|
|
@@ -114,8 +123,13 @@ class Database:
|
|
|
114
123
|
upgrade.execute("DROP TABLE shows")
|
|
115
124
|
upgrade.close()
|
|
116
125
|
|
|
126
|
+
assert isinstance(Show.__table__, Table)
|
|
117
127
|
Show.__table__.create(bind=self.engine)
|
|
128
|
+
|
|
129
|
+
assert isinstance(Episode.__table__, Table)
|
|
118
130
|
Episode.__table__.create(bind=self.engine)
|
|
131
|
+
|
|
132
|
+
assert isinstance(Meta.__table__, Table)
|
|
119
133
|
Meta.__table__.create(bind=self.engine)
|
|
120
134
|
|
|
121
135
|
schema_version = 4
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# episoder, https://code.ott.net/episoder
|
|
4
4
|
#
|
|
5
|
-
# Copyright (C) 2004-
|
|
5
|
+
# Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
|
|
6
6
|
#
|
|
7
7
|
# This program is free software: you can redistribute it and/or modify
|
|
8
8
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -26,6 +26,9 @@ from datetime import date
|
|
|
26
26
|
from traceback import print_exc
|
|
27
27
|
from typing import Any, Callable, Optional
|
|
28
28
|
|
|
29
|
+
from platformdirs import PlatformDirs
|
|
30
|
+
|
|
31
|
+
from .cache import Cache
|
|
29
32
|
from .config import ConfigLoader, EpisoderConfig
|
|
30
33
|
from .database import Show
|
|
31
34
|
from .episoder import Database
|
|
@@ -33,7 +36,8 @@ from .options import OptionsLoader
|
|
|
33
36
|
from .output import ColorfulRenderer, ColorlessRenderer, EmailNotifier
|
|
34
37
|
from .output import ConsoleRenderer, NewEpisodesNotification, SmtpTestMessage
|
|
35
38
|
from .sources import parser_for, setup_sources
|
|
36
|
-
from .sources.
|
|
39
|
+
from .sources.thetvdb_legacy import TVDBLegacy
|
|
40
|
+
from .sources.thetvdb_v4 import TVDBv4
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
def configure_logging(args: Namespace) -> None:
|
|
@@ -65,7 +69,7 @@ class DatabaseWrapper:
|
|
|
65
69
|
self._conn.close()
|
|
66
70
|
|
|
67
71
|
|
|
68
|
-
def with_db(func: Callable) -> Callable:
|
|
72
|
+
def with_db(func: Callable) -> Callable: # type: ignore
|
|
69
73
|
@functools.wraps(func)
|
|
70
74
|
def wrapped(*args: str, **kwargs: str) -> Any:
|
|
71
75
|
self = args[0]
|
|
@@ -220,15 +224,12 @@ class Episoder:
|
|
|
220
224
|
|
|
221
225
|
def search(self, keyword: str) -> None:
|
|
222
226
|
tvdb = parser_for('123') # numeric IDs are TVDB entries
|
|
223
|
-
assert isinstance(tvdb,
|
|
224
|
-
|
|
225
|
-
try:
|
|
226
|
-
print('ID\tName\n-------\t--------------------')
|
|
227
|
-
for show in tvdb.lookup(keyword):
|
|
228
|
-
print(f'{show.url}\t{show.name}')
|
|
227
|
+
assert isinstance(tvdb, (TVDBLegacy, TVDBv4))
|
|
228
|
+
shows = tvdb.lookup(keyword)
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
print('ID\tName\n-------\t--------------------')
|
|
231
|
+
for show in shows:
|
|
232
|
+
print(f'{show.url}\t{show.name}')
|
|
232
233
|
|
|
233
234
|
def _update_shows(self, shows: list[Show], after: Optional[date]) -> None:
|
|
234
235
|
assert self.database is not None
|
|
@@ -237,8 +238,7 @@ class Episoder:
|
|
|
237
238
|
parser = parser_for(show.url)
|
|
238
239
|
assert parser is not None
|
|
239
240
|
parser.parse(show, self.database)
|
|
240
|
-
# pylint: disable=broad-exception-caught
|
|
241
|
-
except Exception:
|
|
241
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
242
242
|
self._log.error('Error parsing %s', show)
|
|
243
243
|
print_exc()
|
|
244
244
|
self.database.rollback()
|
|
@@ -358,6 +358,9 @@ class CommandWrapper:
|
|
|
358
358
|
|
|
359
359
|
|
|
360
360
|
def main() -> None:
|
|
361
|
+
dirs = PlatformDirs('episoder', 'cockroach')
|
|
362
|
+
cache = Cache(dirs.user_cache_dir)
|
|
363
|
+
|
|
361
364
|
loader = OptionsLoader()
|
|
362
365
|
options = loader.parse_cmdline(sys.argv[1:])
|
|
363
366
|
loader.validate(options)
|
|
@@ -367,7 +370,7 @@ def main() -> None:
|
|
|
367
370
|
cfg = config_loader.load(options.c)
|
|
368
371
|
config_loader.validate(cfg)
|
|
369
372
|
|
|
370
|
-
setup_sources(cfg.agent, cfg.tvdb_key)
|
|
373
|
+
setup_sources(cfg.agent, cfg.tvdb_key, cache)
|
|
371
374
|
|
|
372
375
|
command = CommandWrapper(options, cfg)
|
|
373
376
|
command.run(options.func)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# episoder, https://code.ott.net/episoder
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2004-
|
|
3
|
+
# Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# episoder, https://code.ott.net/episoder
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2004-
|
|
3
|
+
# Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# episoder, https://code.ott.net/episoder
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2004-
|
|
3
|
+
# Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
from typing import Optional, List
|
|
19
19
|
import requests
|
|
20
20
|
|
|
21
|
+
from episoder.cache import Cache
|
|
21
22
|
from .epguides import Epguides
|
|
22
|
-
from .
|
|
23
|
+
from .thetvdb_legacy import TVDBLegacy
|
|
24
|
+
from .thetvdb_v4 import TVDBv4
|
|
23
25
|
from .tvcom import TVCom
|
|
24
26
|
from .parser import Parser
|
|
25
27
|
|
|
@@ -28,13 +30,14 @@ session = requests.Session()
|
|
|
28
30
|
PARSERS: List[Parser] = []
|
|
29
31
|
|
|
30
32
|
|
|
31
|
-
def setup_sources(user_agent: str, tvdb_api_key: str) -> None:
|
|
33
|
+
def setup_sources(user_agent: str, tvdb_api_key: str, cache: Cache) -> None:
|
|
32
34
|
session.headers.update({'User-Agent': user_agent})
|
|
33
35
|
|
|
34
36
|
while len(PARSERS) > 0:
|
|
35
37
|
PARSERS.pop()
|
|
36
38
|
|
|
37
|
-
PARSERS.append(
|
|
39
|
+
PARSERS.append(TVDBv4(session, tvdb_api_key, cache))
|
|
40
|
+
PARSERS.append(TVDBLegacy(session, tvdb_api_key))
|
|
38
41
|
PARSERS.append(Epguides(session))
|
|
39
42
|
PARSERS.append(TVCom())
|
|
40
43
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# episoder, https://code.ott.net/episoder
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2004-
|
|
3
|
+
# Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -140,7 +140,7 @@ class Epguides(Parser):
|
|
|
140
140
|
then = self._get_date(day)
|
|
141
141
|
|
|
142
142
|
# prodnum is None, we don't have those
|
|
143
|
-
return Episode(title, season, epnum, then, None, total)
|
|
143
|
+
return Episode(title, int(season), int(epnum), then, None, total)
|
|
144
144
|
|
|
145
145
|
def _load_text_episode(self, line: str) -> Optional[Episode]:
|
|
146
146
|
# Known formatting supported by this fine regex:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# episoder, https://code.ott.net/episoder
|
|
2
2
|
#
|
|
3
|
-
# Copyright (C) 2004-
|
|
3
|
+
# Copyright (C) 2004-2026 Stefan Ott. All rights reserved.
|
|
4
4
|
#
|
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
|
6
6
|
# it under the terms of the GNU General Public License as published by
|