episoder 0.12.0__tar.gz → 1.0.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.0.0/PKG-INFO +110 -0
- episoder-1.0.0/README.md +77 -0
- {episoder-0.12.0 → episoder-1.0.0}/episoder/config.py +1 -1
- {episoder-0.12.0 → episoder-1.0.0}/episoder/database.py +56 -52
- {episoder-0.12.0 → episoder-1.0.0}/episoder/episoder.py +17 -3
- {episoder-0.12.0 → episoder-1.0.0}/episoder/main.py +2 -2
- {episoder-0.12.0 → episoder-1.0.0}/episoder/options.py +1 -1
- {episoder-0.12.0 → episoder-1.0.0}/episoder/output.py +1 -1
- {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/__init__.py +1 -1
- {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/epguides.py +1 -1
- {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/parser.py +1 -1
- {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/thetvdb.py +11 -12
- {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/tvcom.py +1 -1
- {episoder-0.12.0 → episoder-1.0.0}/episoder/version.py +2 -2
- episoder-1.0.0/episoder.egg-info/PKG-INFO +110 -0
- episoder-1.0.0/episoder.egg-info/SOURCES.txt +39 -0
- episoder-1.0.0/episoder.egg-info/requires.txt +14 -0
- {episoder-0.12.0 → episoder-1.0.0}/setup.cfg +6 -1
- 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.1 +0 -250
- 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/examples/home.episoder +0 -30
- 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.0.0}/COPYING +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/episoder/__init__.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/episoder.egg-info/dependency_links.txt +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/episoder.egg-info/entry_points.txt +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/episoder.egg-info/top_level.txt +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/pyproject.toml +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/setup.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_commands.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_config.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_database.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_epguides_parser.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_episode.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_main.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_metainfo.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_migration.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_notify.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_options.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_output.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_show.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_sources.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_tvcom.py +0 -0
- {episoder-0.12.0 → episoder-1.0.0}/test/test_tvdb.py +0 -0
episoder-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: episoder
|
|
3
|
+
Version: 1.0.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
|
+
Project-URL: Source, https://git.ott.net/episoder/
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
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: requests
|
|
22
|
+
Requires-Dist: beautifulsoup4
|
|
23
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: mypy; extra == "test"
|
|
26
|
+
Requires-Dist: flake8; extra == "test"
|
|
27
|
+
Requires-Dist: pytest; extra == "test"
|
|
28
|
+
Requires-Dist: pyfakefs; extra == "test"
|
|
29
|
+
Requires-Dist: types-requests; extra == "test"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# episoder
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
Episoder is a command-line tool that tells you about new episodes of your
|
|
39
|
+
favourite TV shows. It gets its data from online TV episode guides such as
|
|
40
|
+
epguides.com and TheTVDB.
|
|
41
|
+
|
|
42
|
+
There is also a web frontend for episoder, called
|
|
43
|
+
[webisoder](https://code.ott.net/webisoder).
|
|
44
|
+
|
|
45
|
+
## Dependencies
|
|
46
|
+
|
|
47
|
+
In order to use episoder, you need to have Python installed on your system. In
|
|
48
|
+
addition to the default distribution, episoder uses the `requests`, `argparse`,
|
|
49
|
+
`sqlalchemy` and `beautifulsoup4` packages.
|
|
50
|
+
|
|
51
|
+
## Downloading
|
|
52
|
+
|
|
53
|
+
The official website, bug tracker etc. for episoder can be found at
|
|
54
|
+
[code.ott.net/episoder](https://code.ott.net/episoder/).
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
You can install episoder by running `pip install episoder` or you can download
|
|
59
|
+
the tarball from the official website, extract it and run `setup.py install`.
|
|
60
|
+
|
|
61
|
+
## Getting started
|
|
62
|
+
|
|
63
|
+
First, find a show you're interested in:
|
|
64
|
+
|
|
65
|
+
$ episoder search firefly
|
|
66
|
+
ID Name
|
|
67
|
+
------- --------------------
|
|
68
|
+
78874 Firefly
|
|
69
|
+
384248 Firefly Lane
|
|
70
|
+
329088 Firefly (2017)
|
|
71
|
+
|
|
72
|
+
Add that show to episoder:
|
|
73
|
+
|
|
74
|
+
$ episoder add 78874
|
|
75
|
+
|
|
76
|
+
Update your database:
|
|
77
|
+
|
|
78
|
+
$ episoder update
|
|
79
|
+
|
|
80
|
+
And finally show your episodes:
|
|
81
|
+
|
|
82
|
+
$ episoder list
|
|
83
|
+
|
|
84
|
+
The list may be empty, by default episoder deletes old episodes and will only
|
|
85
|
+
show you the next few days. The `--nodate` option to the `update` and `list`
|
|
86
|
+
command will change that, in case you care about days long gone and/or far in
|
|
87
|
+
the future.
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
The configuration file at `~/.episoder` contains your default settings for
|
|
92
|
+
episoder. After installing episoder, the man page will help you with the
|
|
93
|
+
configuration.
|
|
94
|
+
|
|
95
|
+
## Using episoder
|
|
96
|
+
|
|
97
|
+
To use episoder you will typically:
|
|
98
|
+
|
|
99
|
+
* Create a cron job to update your database once a day using `episoder update`
|
|
100
|
+
* Add `episoder list` to your `~/.bashrc`, `~/.bash_profile`, `/etc/bash.bashrc`
|
|
101
|
+
or `/etc/profile` if you want to see your upcoming shows every time you start
|
|
102
|
+
a shell. Consult your shell's documentation to find out which file to use.
|
|
103
|
+
* Or run `episoder notify` at regular intervals to get notifications by e-mail.
|
|
104
|
+
|
|
105
|
+
## More documentation
|
|
106
|
+
|
|
107
|
+
There is some documentation to help you get started:
|
|
108
|
+
|
|
109
|
+
* [Using episoder](https://code.ott.net/episoder/doc/use.html)
|
|
110
|
+
* [Episoder configuration](https://code.ott.net/episoder/doc/config.html)
|
episoder-1.0.0/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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 by running `pip install episoder` or you can download
|
|
26
|
+
the tarball from the official website, extract it and run `setup.py install`.
|
|
27
|
+
|
|
28
|
+
## Getting started
|
|
29
|
+
|
|
30
|
+
First, find a show you're interested in:
|
|
31
|
+
|
|
32
|
+
$ episoder search firefly
|
|
33
|
+
ID Name
|
|
34
|
+
------- --------------------
|
|
35
|
+
78874 Firefly
|
|
36
|
+
384248 Firefly Lane
|
|
37
|
+
329088 Firefly (2017)
|
|
38
|
+
|
|
39
|
+
Add that show to episoder:
|
|
40
|
+
|
|
41
|
+
$ episoder add 78874
|
|
42
|
+
|
|
43
|
+
Update your database:
|
|
44
|
+
|
|
45
|
+
$ episoder update
|
|
46
|
+
|
|
47
|
+
And finally show your episodes:
|
|
48
|
+
|
|
49
|
+
$ episoder list
|
|
50
|
+
|
|
51
|
+
The list may be empty, by default episoder deletes old episodes and will only
|
|
52
|
+
show you the next few days. The `--nodate` option to the `update` and `list`
|
|
53
|
+
command will change that, in case you care about days long gone and/or far in
|
|
54
|
+
the future.
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
The configuration file at `~/.episoder` contains your default settings for
|
|
59
|
+
episoder. After installing episoder, the man page will help you with the
|
|
60
|
+
configuration.
|
|
61
|
+
|
|
62
|
+
## Using episoder
|
|
63
|
+
|
|
64
|
+
To use episoder you will typically:
|
|
65
|
+
|
|
66
|
+
* Create a cron job to update your database once a day using `episoder update`
|
|
67
|
+
* Add `episoder list` to your `~/.bashrc`, `~/.bash_profile`, `/etc/bash.bashrc`
|
|
68
|
+
or `/etc/profile` if you want to see your upcoming shows every time you start
|
|
69
|
+
a shell. Consult your shell's documentation to find out which file to use.
|
|
70
|
+
* Or run `episoder notify` at regular intervals to get notifications by e-mail.
|
|
71
|
+
|
|
72
|
+
## More documentation
|
|
73
|
+
|
|
74
|
+
There is some documentation to help you get started:
|
|
75
|
+
|
|
76
|
+
* [Using episoder](https://code.ott.net/episoder/doc/use.html)
|
|
77
|
+
* [Episoder configuration](https://code.ott.net/episoder/doc/config.html)
|
|
@@ -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
|
|
@@ -15,71 +15,76 @@
|
|
|
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
89
|
self.season = int(season)
|
|
85
90
|
self.episode = int(episode)
|
|
@@ -87,26 +92,28 @@ class Episode(Base):
|
|
|
87
92
|
self.prodnum = prodnum
|
|
88
93
|
self.totalnum = int(total)
|
|
89
94
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
return ((self.season < other.season)
|
|
93
|
-
or ((self.season == other.season)
|
|
94
|
-
and (self.episode < other.episode)))
|
|
95
|
-
|
|
96
|
-
def __str__(self):
|
|
97
|
-
|
|
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
|
|
@@ -65,7 +65,7 @@ class DatabaseWrapper:
|
|
|
65
65
|
self._conn.close()
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def with_db(func: Callable) -> Callable:
|
|
68
|
+
def with_db(func: Callable) -> Callable: # type: ignore
|
|
69
69
|
@functools.wraps(func)
|
|
70
70
|
def wrapped(*args: str, **kwargs: str) -> Any:
|
|
71
71
|
self = args[0]
|
|
@@ -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
|
|
@@ -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
|
|
@@ -19,7 +19,7 @@ from abc import ABC, abstractmethod
|
|
|
19
19
|
from json import dumps
|
|
20
20
|
from logging import getLogger
|
|
21
21
|
from datetime import datetime, date
|
|
22
|
-
from typing import Dict, List, Tuple
|
|
22
|
+
from typing import Any, Dict, List, Tuple
|
|
23
23
|
|
|
24
24
|
from requests import Session
|
|
25
25
|
|
|
@@ -132,29 +132,29 @@ class TVDBOnline(TVDBState):
|
|
|
132
132
|
def __repr__(self) -> str:
|
|
133
133
|
return '<TVDBOnline>'
|
|
134
134
|
|
|
135
|
-
def _get(self, url: str, params: Dict) -> Dict:
|
|
135
|
+
def _get(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
136
136
|
url = f'https://api.thetvdb.com/{url}'
|
|
137
137
|
head = {'Content-type': 'application/json',
|
|
138
138
|
'Authorization': f'Bearer {self._token}'}
|
|
139
139
|
|
|
140
140
|
response = self._session.get(url, headers=head, params=params,
|
|
141
141
|
timeout=10)
|
|
142
|
-
data = response.json()
|
|
142
|
+
data: Dict[str, Any] = response.json()
|
|
143
143
|
|
|
144
144
|
if response.status_code == 404:
|
|
145
145
|
raise TVDBShowNotFoundError(data.get('Error'))
|
|
146
146
|
|
|
147
147
|
return data
|
|
148
148
|
|
|
149
|
-
def _get_episodes(self, show: Show,
|
|
150
|
-
|
|
149
|
+
def _get_episodes(self, show: Show, page: int) -> Tuple[
|
|
150
|
+
List[Dict[str, str]], Dict[str, int]]:
|
|
151
151
|
show_id = int(show.url)
|
|
152
152
|
opts = {'page': page}
|
|
153
153
|
result = self._get(f'series/{show_id}/episodes', opts)
|
|
154
154
|
return (result.get('data') or [], result.get('links', {}))
|
|
155
155
|
|
|
156
156
|
def lookup(self, text: str) -> List[Show]:
|
|
157
|
-
def mkshow(entry: Dict) -> Show:
|
|
157
|
+
def mkshow(entry: Dict[str, str]) -> Show:
|
|
158
158
|
name = entry.get('seriesName')
|
|
159
159
|
assert isinstance(name, str)
|
|
160
160
|
url = str(entry.get('id')).encode('utf8').decode('utf8')
|
|
@@ -167,7 +167,7 @@ class TVDBOnline(TVDBState):
|
|
|
167
167
|
pass
|
|
168
168
|
|
|
169
169
|
def _fetch_episodes(self, show: Show, page: int) -> List[Episode]:
|
|
170
|
-
def airdate(row: Dict) -> date:
|
|
170
|
+
def airdate(row: Dict[str, str]) -> date:
|
|
171
171
|
aired = row.get('firstAired', '1970-01-01')
|
|
172
172
|
|
|
173
173
|
if aired == '0000-00-00':
|
|
@@ -175,7 +175,7 @@ class TVDBOnline(TVDBState):
|
|
|
175
175
|
|
|
176
176
|
return datetime.strptime(aired, '%Y-%m-%d').date()
|
|
177
177
|
|
|
178
|
-
def mkepisode(row: Dict) -> Episode:
|
|
178
|
+
def mkepisode(row: Dict[str, str]) -> Episode:
|
|
179
179
|
num = int(row.get("airedEpisodeNumber", "0") or 0)
|
|
180
180
|
name = row.get('episodeName') or 'Unnamed episode'
|
|
181
181
|
season = int(row.get('airedSeason', '0') or 0)
|
|
@@ -185,12 +185,11 @@ class TVDBOnline(TVDBState):
|
|
|
185
185
|
self._log.debug('Found episode %s', name)
|
|
186
186
|
return Episode(name, season, num, aired, pnum, 0)
|
|
187
187
|
|
|
188
|
-
def is_valid(row: Dict) -> bool:
|
|
188
|
+
def is_valid(row: Dict[str, str]) -> bool:
|
|
189
189
|
return row.get("firstAired") not in [None, ""]
|
|
190
190
|
|
|
191
191
|
(data, links) = self._get_episodes(show, page)
|
|
192
|
-
|
|
193
|
-
episodes = [mkepisode(row) for row in valid]
|
|
192
|
+
episodes = [mkepisode(row) for row in data if is_valid(row)]
|
|
194
193
|
|
|
195
194
|
# handle pagination
|
|
196
195
|
next_ = links.get("next") or 0
|
|
@@ -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
|
|
@@ -15,4 +15,4 @@
|
|
|
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
|
-
__version__ = "0.
|
|
18
|
+
__version__ = "1.0.0"
|