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.
Files changed (88) hide show
  1. episoder-1.0.0/PKG-INFO +110 -0
  2. episoder-1.0.0/README.md +77 -0
  3. {episoder-0.12.0 → episoder-1.0.0}/episoder/config.py +1 -1
  4. {episoder-0.12.0 → episoder-1.0.0}/episoder/database.py +56 -52
  5. {episoder-0.12.0 → episoder-1.0.0}/episoder/episoder.py +17 -3
  6. {episoder-0.12.0 → episoder-1.0.0}/episoder/main.py +2 -2
  7. {episoder-0.12.0 → episoder-1.0.0}/episoder/options.py +1 -1
  8. {episoder-0.12.0 → episoder-1.0.0}/episoder/output.py +1 -1
  9. {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/__init__.py +1 -1
  10. {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/epguides.py +1 -1
  11. {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/parser.py +1 -1
  12. {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/thetvdb.py +11 -12
  13. {episoder-0.12.0 → episoder-1.0.0}/episoder/sources/tvcom.py +1 -1
  14. {episoder-0.12.0 → episoder-1.0.0}/episoder/version.py +2 -2
  15. episoder-1.0.0/episoder.egg-info/PKG-INFO +110 -0
  16. episoder-1.0.0/episoder.egg-info/SOURCES.txt +39 -0
  17. episoder-1.0.0/episoder.egg-info/requires.txt +14 -0
  18. {episoder-0.12.0 → episoder-1.0.0}/setup.cfg +6 -1
  19. episoder-0.12.0/.gitignore +0 -2
  20. episoder-0.12.0/.gitlab-ci.yml +0 -150
  21. episoder-0.12.0/CHANGELOG +0 -228
  22. episoder-0.12.0/PKG-INFO +0 -74
  23. episoder-0.12.0/README.md +0 -47
  24. episoder-0.12.0/episoder.1 +0 -250
  25. episoder-0.12.0/episoder.egg-info/PKG-INFO +0 -74
  26. episoder-0.12.0/episoder.egg-info/SOURCES.txt +0 -82
  27. episoder-0.12.0/episoder.egg-info/requires.txt +0 -8
  28. episoder-0.12.0/examples/home.episoder +0 -30
  29. episoder-0.12.0/test/__init__.py +0 -0
  30. episoder-0.12.0/test/fixtures/epguides_48_hours_mistery.html +0 -399
  31. episoder-0.12.0/test/fixtures/epguides_american_idol.html +0 -872
  32. episoder-0.12.0/test/fixtures/epguides_bsg.html +0 -538
  33. episoder-0.12.0/test/fixtures/epguides_buzzcocks.html +0 -464
  34. episoder-0.12.0/test/fixtures/epguides_doctor_who.html +0 -842
  35. episoder-0.12.0/test/fixtures/epguides_eureka.html +0 -362
  36. episoder-0.12.0/test/fixtures/epguides_futurama.html +0 -407
  37. episoder-0.12.0/test/fixtures/epguides_house.html +0 -490
  38. episoder-0.12.0/test/fixtures/epguides_kr2008.html +0 -256
  39. episoder-0.12.0/test/fixtures/epguides_lost.html +0 -478
  40. episoder-0.12.0/test/fixtures/epguides_lost_html.html +0 -602
  41. episoder-0.12.0/test/fixtures/epguides_lower_decks.html +0 -340
  42. episoder-0.12.0/test/fixtures/epguides_midsomer_murders.html +0 -398
  43. episoder-0.12.0/test/fixtures/epguides_picard.html +0 -380
  44. episoder-0.12.0/test/fixtures/epguides_test_iso_8859_1.html +0 -138
  45. episoder-0.12.0/test/fixtures/tvdb_error_404.json +0 -3
  46. episoder-0.12.0/test/fixtures/tvdb_search_frasier.json +0 -10
  47. episoder-0.12.0/test/fixtures/tvdb_search_friends.json +0 -28
  48. episoder-0.12.0/test/fixtures/tvdb_test_show_260.json +0 -1
  49. episoder-0.12.0/test/fixtures/tvdb_test_show_260_1_eps.json +0 -19
  50. episoder-0.12.0/test/fixtures/tvdb_test_show_261.json +0 -1
  51. episoder-0.12.0/test/fixtures/tvdb_test_show_261_1_eps.json +0 -23
  52. episoder-0.12.0/test/fixtures/tvdb_test_show_261_2_eps.json +0 -23
  53. episoder-0.12.0/test/fixtures/tvdb_test_show_261_3_eps.json +0 -18
  54. episoder-0.12.0/test/fixtures/tvdb_test_show_262.json +0 -1
  55. episoder-0.12.0/test/fixtures/tvdb_test_show_262_1_eps.json +0 -20
  56. episoder-0.12.0/test/fixtures/tvdb_test_show_263.json +0 -1
  57. episoder-0.12.0/test/fixtures/tvdb_test_show_268156.json +0 -1
  58. episoder-0.12.0/test/fixtures/tvdb_test_show_268156_1_eps.json +0 -520
  59. episoder-0.12.0/test/fixtures/tvdb_test_show_295640.json +0 -1
  60. episoder-0.12.0/test/fixtures/tvdb_test_show_295640_1_eps.json +0 -1
  61. episoder-0.12.0/test/fixtures/tvdb_test_show_295640_2_eps.json +0 -1
  62. episoder-0.12.0/test/fixtures/tvdb_test_show_73739.json +0 -1
  63. episoder-0.12.0/test/fixtures/tvdb_test_show_73739_1_eps.json +0 -14
  64. episoder-0.12.0/test/fixtures/tvdb_test_show_75397.json +0 -1
  65. episoder-0.12.0/test/fixtures/tvdb_test_show_75397_1_eps.json +0 -1
  66. episoder-0.12.0/test/util.py +0 -54
  67. {episoder-0.12.0 → episoder-1.0.0}/COPYING +0 -0
  68. {episoder-0.12.0 → episoder-1.0.0}/episoder/__init__.py +0 -0
  69. {episoder-0.12.0 → episoder-1.0.0}/episoder.egg-info/dependency_links.txt +0 -0
  70. {episoder-0.12.0 → episoder-1.0.0}/episoder.egg-info/entry_points.txt +0 -0
  71. {episoder-0.12.0 → episoder-1.0.0}/episoder.egg-info/top_level.txt +0 -0
  72. {episoder-0.12.0 → episoder-1.0.0}/pyproject.toml +0 -0
  73. {episoder-0.12.0 → episoder-1.0.0}/setup.py +0 -0
  74. {episoder-0.12.0 → episoder-1.0.0}/test/test_commands.py +0 -0
  75. {episoder-0.12.0 → episoder-1.0.0}/test/test_config.py +0 -0
  76. {episoder-0.12.0 → episoder-1.0.0}/test/test_database.py +0 -0
  77. {episoder-0.12.0 → episoder-1.0.0}/test/test_epguides_parser.py +0 -0
  78. {episoder-0.12.0 → episoder-1.0.0}/test/test_episode.py +0 -0
  79. {episoder-0.12.0 → episoder-1.0.0}/test/test_main.py +0 -0
  80. {episoder-0.12.0 → episoder-1.0.0}/test/test_metainfo.py +0 -0
  81. {episoder-0.12.0 → episoder-1.0.0}/test/test_migration.py +0 -0
  82. {episoder-0.12.0 → episoder-1.0.0}/test/test_notify.py +0 -0
  83. {episoder-0.12.0 → episoder-1.0.0}/test/test_options.py +0 -0
  84. {episoder-0.12.0 → episoder-1.0.0}/test/test_output.py +0 -0
  85. {episoder-0.12.0 → episoder-1.0.0}/test/test_show.py +0 -0
  86. {episoder-0.12.0 → episoder-1.0.0}/test/test_sources.py +0 -0
  87. {episoder-0.12.0 → episoder-1.0.0}/test/test_tvcom.py +0 -0
  88. {episoder-0.12.0 → episoder-1.0.0}/test/test_tvdb.py +0 -0
@@ -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
+ ![episoder](https://code.ott.net/episoder/screenshot.png "episoder 0.8.0")
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)
@@ -0,0 +1,77 @@
1
+ # episoder
2
+
3
+ ![episoder](https://code.ott.net/episoder/screenshot.png "episoder 0.8.0")
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-2025 Stefan Ott. All rights reserved.
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-2024 Stefan Ott. All rights reserved.
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 Column, Integer, Text, Date, ForeignKey, Sequence
21
- from sqlalchemy import DateTime, Boolean
22
- from sqlalchemy.orm import backref, declarative_base, relationship
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 = Column("show_id", Integer, Sequence("shows_show_id_seq"),
36
- primary_key=True)
37
- name = Column("show_name", Text)
38
- url = Column(Text, unique=True)
39
- updated = Column(DateTime)
40
- enabled = Column(Boolean)
41
- status = Column(Integer, default=RUNNING)
42
-
43
- def __init__(self, name, url="", updated=datetime.fromtimestamp(0)):
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 = Column(Integer, ForeignKey(Show.id), primary_key=True)
71
- episode = Column("num", Integer, primary_key=True)
72
- airdate = Column(Date)
73
- season = Column(Integer, primary_key=True)
74
- title = Column(Text)
75
- totalnum = Column(Integer)
76
- prodnum = Column(Text)
77
- notified = Column(Date)
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, prodnum, total):
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 __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):
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 __eq__(self, other):
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 = Column(Text, primary_key=True)
120
- value = Column(Text)
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-2024 Stefan Ott. All rights reserved.
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 if inspector.has_table(t.__table__.name)]
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-2025 Stefan Ott. All rights reserved.
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-2025 Stefan Ott. All rights reserved.
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-2025 Stefan Ott. All rights reserved.
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-2024 Stefan Ott. All rights reserved.
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-2024 Stefan Ott. All rights reserved.
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-2024 Stefan Ott. All rights reserved.
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-2024 Stefan Ott. All rights reserved.
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
- page: int) -> Tuple[List[Episode], Dict]:
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
- valid: filter = filter(is_valid, data)
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-2024 Stefan Ott. All rights reserved.
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-2025 Stefan Ott. All rights reserved.
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.12.0"
18
+ __version__ = "1.0.0"