episoder 0.11.0__py3-none-any.whl
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/__init__.py +0 -0
- episoder/database.py +134 -0
- episoder/episoder.py +292 -0
- episoder/main.py +469 -0
- episoder/output.py +186 -0
- episoder/sources/__init__.py +47 -0
- episoder/sources/epguides.py +174 -0
- episoder/sources/parser.py +30 -0
- episoder/sources/thetvdb.py +231 -0
- episoder/sources/tvcom.py +43 -0
- episoder/version.py +18 -0
- episoder-0.11.0.dist-info/COPYING +674 -0
- episoder-0.11.0.dist-info/METADATA +71 -0
- episoder-0.11.0.dist-info/RECORD +17 -0
- episoder-0.11.0.dist-info/WHEEL +5 -0
- episoder-0.11.0.dist-info/entry_points.txt +2 -0
- episoder-0.11.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# episoder, https://code.ott.net/episoder
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2004-2024 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 datetime import datetime, date
|
|
19
|
+
from logging import getLogger
|
|
20
|
+
from re import search
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from bs4 import BeautifulSoup, element
|
|
24
|
+
from requests import Response, Session
|
|
25
|
+
|
|
26
|
+
from episoder.database import Episode, Show
|
|
27
|
+
from episoder.episoder import Database
|
|
28
|
+
from .parser import Parser
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Epguides(Parser):
|
|
32
|
+
"""Parser for episodes from epguides.com"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, session: Session) -> None:
|
|
35
|
+
self._logger = getLogger('Epguides')
|
|
36
|
+
self._session = session
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return 'epguides.com parser'
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
return 'Epguides()'
|
|
43
|
+
|
|
44
|
+
def accept(self, url: str) -> bool:
|
|
45
|
+
return 'epguides.com/' in url
|
|
46
|
+
|
|
47
|
+
def _guess_encoding(self, response: Response) -> str:
|
|
48
|
+
raw = response.raw.read()
|
|
49
|
+
text = raw.decode('iso-8859-1')
|
|
50
|
+
|
|
51
|
+
if 'charset=iso-8859-1' in text:
|
|
52
|
+
return 'iso-8859-1'
|
|
53
|
+
|
|
54
|
+
return 'utf8'
|
|
55
|
+
|
|
56
|
+
def parse(self, show: Show, db: Database) -> None:
|
|
57
|
+
response = self._session.get(show.url, timeout=10)
|
|
58
|
+
response.encoding = self._guess_encoding(response)
|
|
59
|
+
|
|
60
|
+
for line in response.text.split('\n'):
|
|
61
|
+
self._parse_line(line, show, db)
|
|
62
|
+
|
|
63
|
+
show.updated = datetime.now()
|
|
64
|
+
db.commit()
|
|
65
|
+
|
|
66
|
+
def _html_episode_title(self, soup: BeautifulSoup) -> str:
|
|
67
|
+
cell = soup.find('td', class_='eptitle')
|
|
68
|
+
if isinstance(cell, element.Tag):
|
|
69
|
+
link = cell.find('a')
|
|
70
|
+
if isinstance(link, element.Tag):
|
|
71
|
+
return link.text.strip()
|
|
72
|
+
return ''
|
|
73
|
+
|
|
74
|
+
def _update_show_title(self, show: Show, line: str) -> None:
|
|
75
|
+
# Name of the show
|
|
76
|
+
res = search(r'<title>(.*)</title>', line)
|
|
77
|
+
if res:
|
|
78
|
+
title = res.groups()[0]
|
|
79
|
+
show.name = title.split(' (a ')[0]
|
|
80
|
+
|
|
81
|
+
def _update_show_status(self, show: Show, line: str) -> None:
|
|
82
|
+
# Current status (running / ended)
|
|
83
|
+
res = search(r'<span class="status">(.*)</span>', line)
|
|
84
|
+
if res:
|
|
85
|
+
text = res.groups()[0]
|
|
86
|
+
if 'current' in text:
|
|
87
|
+
show.status = Show.RUNNING
|
|
88
|
+
else:
|
|
89
|
+
show.status = Show.ENDED
|
|
90
|
+
elif search(r'aired.*to.*[\d+]', line):
|
|
91
|
+
show.status = Show.ENDED
|
|
92
|
+
|
|
93
|
+
# Current status in new HTML version
|
|
94
|
+
res = search(r'Status: (.*)<br', line)
|
|
95
|
+
if res:
|
|
96
|
+
text = res.groups()[0]
|
|
97
|
+
if 'ended' in text:
|
|
98
|
+
show.status = Show.ENDED
|
|
99
|
+
else:
|
|
100
|
+
show.status = Show.RUNNING
|
|
101
|
+
|
|
102
|
+
def _get_date(self, val: str) -> date:
|
|
103
|
+
day = val.replace('/', ' ')
|
|
104
|
+
then = datetime.strptime(day, '%d %b %y')
|
|
105
|
+
return then.date()
|
|
106
|
+
|
|
107
|
+
def _load_html_episode(self, line: str) -> Optional[Episode]:
|
|
108
|
+
soup = BeautifulSoup(line, 'html.parser')
|
|
109
|
+
cells = soup.find_all('td', class_='epinfo')
|
|
110
|
+
|
|
111
|
+
# We expect 3 cells, otherwise we refuse to go on
|
|
112
|
+
if len(cells) < 3:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
# total episode number
|
|
116
|
+
total_episodes = cells[0].text.split('.', 1)[0]
|
|
117
|
+
try:
|
|
118
|
+
total = int(total_episodes)
|
|
119
|
+
except ValueError:
|
|
120
|
+
# Some shows have specials (numbered separately)
|
|
121
|
+
total = -1
|
|
122
|
+
|
|
123
|
+
# season and episode number
|
|
124
|
+
details = cells[1].text.split('-')
|
|
125
|
+
if len(details) < 2:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
[season, epnum] = details
|
|
129
|
+
|
|
130
|
+
# episode title
|
|
131
|
+
title = self._html_episode_title(soup)
|
|
132
|
+
|
|
133
|
+
# original air date
|
|
134
|
+
day = cells[2].text
|
|
135
|
+
if not day:
|
|
136
|
+
# Drop episodes without date
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
then = self._get_date(day)
|
|
140
|
+
|
|
141
|
+
# prodnum is None, we don't have those
|
|
142
|
+
return Episode(title, season, epnum, then, None, total)
|
|
143
|
+
|
|
144
|
+
def _load_text_episode(self, line: str) -> Optional[Episode]:
|
|
145
|
+
# Known formatting supported by this fine regex:
|
|
146
|
+
# 4. 1-4 19 Jun 02 <a [..]>title</a>
|
|
147
|
+
# 1. 19- 1 01-01 5 Jan 88 <a [..]>title</a>
|
|
148
|
+
# 23 3-05 27/Mar/98 <a [..]>title</a>
|
|
149
|
+
# 65. 17-10 23 Apr 05 <a [..]>title</a>
|
|
150
|
+
# 101. 5-15 09 May 09 <a [..]>title</a>
|
|
151
|
+
# 254. - 5 05-254 15 Jan 92 <a [..]>title</a>
|
|
152
|
+
res = search(r'^ *(\d+)\.? +(\d*)- ?(\d+) +([a-zA-Z0-9-]*)'
|
|
153
|
+
r' +(\d{1,2}[ /][A-Z][a-z]{2}[ /]\d{2}) *<a.*>(.*)</a>',
|
|
154
|
+
line)
|
|
155
|
+
|
|
156
|
+
if not res:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
(total, season, epnum, prodnum, day, title) = res.groups()
|
|
160
|
+
then = self._get_date(day)
|
|
161
|
+
return Episode(title, season or 0, epnum, then, prodnum, total)
|
|
162
|
+
|
|
163
|
+
def _parse_line(self, line: str, show: Show, db: Database) -> None:
|
|
164
|
+
self._update_show_title(show, line)
|
|
165
|
+
self._update_show_status(show, line)
|
|
166
|
+
|
|
167
|
+
if search(r'<td class=.epinfo', line):
|
|
168
|
+
episode = self._load_html_episode(line)
|
|
169
|
+
else:
|
|
170
|
+
episode = self._load_text_episode(line)
|
|
171
|
+
|
|
172
|
+
if episode:
|
|
173
|
+
self._logger.debug('Found episode %s', episode.title)
|
|
174
|
+
db.add_episode(episode, show)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# episoder, https://code.ott.net/episoder
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2004-2024 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 abc import ABC, abstractmethod
|
|
19
|
+
|
|
20
|
+
from episoder.episoder import Database, Show
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Parser(ABC):
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def accept(self, url: str) -> bool:
|
|
26
|
+
"""Test if this parser can load the given URL"""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def parse(self, show: Show, db: Database) -> None:
|
|
30
|
+
"""Load episodes with this parser"""
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# episoder, https://code.ott.net/episoder
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2004-2024 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 abc import ABC, abstractmethod
|
|
19
|
+
from json import dumps
|
|
20
|
+
from logging import getLogger
|
|
21
|
+
from datetime import datetime, date
|
|
22
|
+
from typing import Dict, List, Tuple
|
|
23
|
+
|
|
24
|
+
from requests import Session
|
|
25
|
+
|
|
26
|
+
from episoder.database import Episode, Show
|
|
27
|
+
from episoder.episoder import Database
|
|
28
|
+
from .parser import Parser
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TVDBState(ABC):
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def login(self, key: str) -> None:
|
|
34
|
+
"""Authenticate to the backend"""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def lookup(self, text: str) -> List[Show]:
|
|
38
|
+
"""Find show(s) by name"""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def parse(self, show: Show, db: Database) -> None:
|
|
42
|
+
"""Load episodes with this parser"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InvalidLoginError(Exception):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TVDBShowNotFoundError(Exception):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TVDBNotLoggedInError(Exception):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TVDB(Parser):
|
|
58
|
+
"""Parser for episodes from thetvdb.com"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, session: Session, api_key: str) -> None:
|
|
61
|
+
self._session = session
|
|
62
|
+
self._api_key = api_key
|
|
63
|
+
self._state: TVDBState = TVDBOffline(self, session)
|
|
64
|
+
|
|
65
|
+
def __str__(self) -> str:
|
|
66
|
+
return str(self._state)
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
return f'TVDB {repr(self._state)}'
|
|
70
|
+
|
|
71
|
+
def lookup(self, text: str) -> List[Show]:
|
|
72
|
+
self._state.login(self._api_key)
|
|
73
|
+
return self._state.lookup(text)
|
|
74
|
+
|
|
75
|
+
def parse(self, show: Show, db: Database) -> None:
|
|
76
|
+
self._state.login(self._api_key)
|
|
77
|
+
self._state.parse(show, db)
|
|
78
|
+
|
|
79
|
+
def change(self, state: TVDBState) -> None:
|
|
80
|
+
self._state = state
|
|
81
|
+
|
|
82
|
+
def accept(self, url: str) -> bool:
|
|
83
|
+
return url.isdigit()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TVDBOffline(TVDBState):
|
|
87
|
+
def __init__(self, tvdb: TVDB, session: Session) -> None:
|
|
88
|
+
self._tvdb = tvdb
|
|
89
|
+
self._log = getLogger('TVDB (offline)')
|
|
90
|
+
self._session = session
|
|
91
|
+
|
|
92
|
+
def __str__(self) -> str:
|
|
93
|
+
return 'thetvdb.com parser (ready)'
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return '<TVDBOffline>'
|
|
97
|
+
|
|
98
|
+
def _post_login(self, data: Dict[str, str]) -> str:
|
|
99
|
+
url = 'https://api.thetvdb.com/login'
|
|
100
|
+
head = {'Content-type': 'application/json'}
|
|
101
|
+
body = dumps(data).encode('utf8')
|
|
102
|
+
response = self._session.post(url, body, headers=head, timeout=10)
|
|
103
|
+
data = response.json()
|
|
104
|
+
|
|
105
|
+
if response.status_code == 401:
|
|
106
|
+
raise InvalidLoginError(data.get('Error'))
|
|
107
|
+
|
|
108
|
+
self._log.info('Successful login')
|
|
109
|
+
return data['token']
|
|
110
|
+
|
|
111
|
+
def lookup(self, _: str) -> List[Show]:
|
|
112
|
+
raise TVDBNotLoggedInError()
|
|
113
|
+
|
|
114
|
+
def login(self, key: str) -> None:
|
|
115
|
+
body = {'apikey': key}
|
|
116
|
+
token = self._post_login(body)
|
|
117
|
+
self._tvdb.change(TVDBOnline(token, self._session))
|
|
118
|
+
|
|
119
|
+
def parse(self, _: Show, __: Database) -> None:
|
|
120
|
+
raise TVDBNotLoggedInError()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TVDBOnline(TVDBState):
|
|
124
|
+
def __init__(self, token: str, session: Session) -> None:
|
|
125
|
+
self._token = token
|
|
126
|
+
self._log = getLogger('TVDB (online)')
|
|
127
|
+
self._session = session
|
|
128
|
+
|
|
129
|
+
def __str__(self) -> str:
|
|
130
|
+
return 'thetvdb.com parser (authorized)'
|
|
131
|
+
|
|
132
|
+
def __repr__(self) -> str:
|
|
133
|
+
return '<TVDBOnline>'
|
|
134
|
+
|
|
135
|
+
def _get(self, url: str, params: Dict) -> Dict:
|
|
136
|
+
url = f'https://api.thetvdb.com/{url}'
|
|
137
|
+
head = {'Content-type': 'application/json',
|
|
138
|
+
'Authorization': f'Bearer {self._token}'}
|
|
139
|
+
|
|
140
|
+
response = self._session.get(url, headers=head, params=params,
|
|
141
|
+
timeout=10)
|
|
142
|
+
data = response.json()
|
|
143
|
+
|
|
144
|
+
if response.status_code == 404:
|
|
145
|
+
raise TVDBShowNotFoundError(data.get('Error'))
|
|
146
|
+
|
|
147
|
+
return data
|
|
148
|
+
|
|
149
|
+
def _get_episodes(self, show: Show,
|
|
150
|
+
page: int) -> Tuple[List[Episode], Dict]:
|
|
151
|
+
show_id = int(show.url)
|
|
152
|
+
opts = {'page': page}
|
|
153
|
+
result = self._get(f'series/{show_id}/episodes', opts)
|
|
154
|
+
return (result.get('data') or [], result.get('links', {}))
|
|
155
|
+
|
|
156
|
+
def lookup(self, text: str) -> List[Show]:
|
|
157
|
+
def mkshow(entry: Dict) -> Show:
|
|
158
|
+
name = entry.get('seriesName')
|
|
159
|
+
url = str(entry.get('id')).encode('utf8').decode('utf8')
|
|
160
|
+
return Show(name, url=url)
|
|
161
|
+
|
|
162
|
+
matches = self._get('search/series', {'name': text})
|
|
163
|
+
return list(map(mkshow, matches.get('data', {})))
|
|
164
|
+
|
|
165
|
+
def login(self, _: str) -> None:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def _fetch_episodes(self, show: Show, page: int) -> List[Episode]:
|
|
169
|
+
def airdate(row: Dict) -> date:
|
|
170
|
+
aired = row.get('firstAired', '1970-01-01')
|
|
171
|
+
|
|
172
|
+
if aired == '0000-00-00':
|
|
173
|
+
return datetime(1970, 1, 1)
|
|
174
|
+
|
|
175
|
+
return datetime.strptime(aired, '%Y-%m-%d').date()
|
|
176
|
+
|
|
177
|
+
def mkepisode(row: Dict) -> Episode:
|
|
178
|
+
num = int(row.get("airedEpisodeNumber", "0") or 0)
|
|
179
|
+
name = row.get('episodeName') or 'Unnamed episode'
|
|
180
|
+
season = int(row.get('airedSeason', '0') or 0)
|
|
181
|
+
aired = airdate(row)
|
|
182
|
+
pnum = 'UNK'
|
|
183
|
+
|
|
184
|
+
self._log.debug('Found episode %s', name)
|
|
185
|
+
return Episode(name, season, num, aired, pnum, 0)
|
|
186
|
+
|
|
187
|
+
def is_valid(row: Dict) -> bool:
|
|
188
|
+
return row.get("firstAired") not in [None, ""]
|
|
189
|
+
|
|
190
|
+
(data, links) = self._get_episodes(show, page)
|
|
191
|
+
valid = filter(is_valid, data)
|
|
192
|
+
episodes = [mkepisode(row) for row in valid]
|
|
193
|
+
|
|
194
|
+
# handle pagination
|
|
195
|
+
next_ = links.get("next") or 0
|
|
196
|
+
if next_ > page:
|
|
197
|
+
try:
|
|
198
|
+
more = self._fetch_episodes(show, next_)
|
|
199
|
+
episodes.extend(more)
|
|
200
|
+
except TVDBShowNotFoundError:
|
|
201
|
+
msg = "Error parsing %s: failed to load page %d"
|
|
202
|
+
self._log.error(msg, show.name, next_)
|
|
203
|
+
|
|
204
|
+
return episodes
|
|
205
|
+
|
|
206
|
+
def parse(self, show: Show, db: Database) -> None:
|
|
207
|
+
result = self._get(f'series/{int(show.url)}', {})
|
|
208
|
+
data = result.get('data', {})
|
|
209
|
+
|
|
210
|
+
# update show data
|
|
211
|
+
show.name = data.get('seriesName', show.name)
|
|
212
|
+
show.updated = datetime.now()
|
|
213
|
+
|
|
214
|
+
self._log.debug("Updating show '%s'", show.name)
|
|
215
|
+
|
|
216
|
+
if data.get('status') == 'Continuing':
|
|
217
|
+
show.status = Show.RUNNING
|
|
218
|
+
else:
|
|
219
|
+
show.status = Show.ENDED
|
|
220
|
+
|
|
221
|
+
# load episodes
|
|
222
|
+
try:
|
|
223
|
+
episodes = self._fetch_episodes(show, 1)
|
|
224
|
+
except TVDBShowNotFoundError:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
for (idx, episode) in enumerate(sorted(episodes)):
|
|
228
|
+
episode.totalnum = idx + 1
|
|
229
|
+
db.add_episode(episode, show)
|
|
230
|
+
|
|
231
|
+
db.commit()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# episoder, https://code.ott.net/episoder
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2004-2024 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 logging
|
|
19
|
+
from re import match
|
|
20
|
+
|
|
21
|
+
from episoder.database import Show
|
|
22
|
+
from episoder.episoder import Database
|
|
23
|
+
from .parser import Parser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TVCom(Parser):
|
|
27
|
+
"""Parser for episodes from tv.com"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._log = logging.getLogger('TVCom')
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
return 'dummy tv.com parser to detect old urls'
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return 'TVCom()'
|
|
37
|
+
|
|
38
|
+
def accept(self, url: str) -> bool:
|
|
39
|
+
exp = 'http://(www.)?tv.com/.*'
|
|
40
|
+
return match(exp, url) is not None
|
|
41
|
+
|
|
42
|
+
def parse(self, show: Show, _: Database) -> None:
|
|
43
|
+
self._log.error("The url %s is no longer supported", show.url)
|
episoder/version.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# episoder, https://code.ott.net/episoder
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2004-2024 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
|
+
__version__ = "0.11.0"
|