twitch-archiver 3.0.0.dev1__tar.gz → 3.0.0.dev5__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 (23) hide show
  1. {twitch-archiver-3.0.0.dev1/twitch_archiver.egg-info → twitch-archiver-3.0.0.dev5}/PKG-INFO +14 -10
  2. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/README.md +13 -9
  3. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/pyproject.toml +1 -1
  4. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5/twitch_archiver.egg-info}/PKG-INFO +14 -10
  5. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/__init__.py +16 -12
  6. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/arguments.py +1 -1
  7. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/processing.py +24 -19
  8. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/stream.py +18 -7
  9. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/utils.py +73 -38
  10. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/LICENSE +0 -0
  11. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/setup.cfg +0 -0
  12. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/setup.py +0 -0
  13. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/SOURCES.txt +0 -0
  14. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/dependency_links.txt +0 -0
  15. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/entry_points.txt +0 -0
  16. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/top_level.txt +0 -0
  17. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/api.py +0 -0
  18. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/configuration.py +0 -0
  19. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/database.py +0 -0
  20. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/downloader.py +0 -0
  21. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/exceptions.py +0 -0
  22. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/logger.py +0 -0
  23. {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/twitch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: twitch-archiver
3
- Version: 3.0.0.dev1
3
+ Version: 3.0.0.dev5
4
4
  Summary: A simple, fast, platform-independent tool for downloading Twitch streams, videos, and chat logs.
5
5
  Author-email: Brisppy <brisppy@protonmail.com>
6
6
  Project-URL: Homepage, https://github.com/Brisppy/twitch-archiver
@@ -40,7 +40,7 @@ Primarily focused on data preservation, this script can be used to archive an en
40
40
  ## Features
41
41
  * Archives both video and chat logs.
42
42
  * VODs can be downloaded as fast as your Internet connection (and storage) can handle.[^1]
43
- * Allows the downloading of live VODs before sections are muted or deleted.[^2]
43
+ * Allows real-time archiving of Twitch streams.[^2]
44
44
  * Generates a readable chat log with timestamps and user badges.
45
45
  * Supports downloading streams which aren't being archived by Twitch.
46
46
  * Error notifications supported with pushbullet.
@@ -68,8 +68,9 @@ Make sure to read the [usage](#usage) section after installation.
68
68
 
69
69
  1. Either download the repository via the green code button at the top of the page, or grab the latest release [here](https://github.com/Brisppy/twitch-archiver/releases/latest).
70
70
  2. Unpack the archive and enter the directory with `cd twitch-archiver`.
71
- 2. Install [pip](https://pip.pypa.io/en/stable/installation/) if you do not already have it.
72
- 3. Build the package with `python -m build`, then install with `python -m pip install ./dist/twitch-archiver-*.tar.gz`.
71
+ 3. Install [pip](https://pip.pypa.io/en/stable/installation/) if you do not already have it.
72
+ 4. Install Python "Build" package with `python -m pip install --upgrade build`.
73
+ 5. Build the package with `python -m build`, then install with `python -m pip install ./dist/twitch-archiver-*.tar.gz`.
73
74
 
74
75
  #### Installing as a Docker Container
75
76
  1. Either download the repository via the green code button at the top of the page, or grab the latest release [here](https://github.com/Brisppy/twitch-archiver/releases/latest).
@@ -102,9 +103,9 @@ Would download VODs **1276315849** and **1275305106** to the directory **/mnt/tw
102
103
  #### Arguments
103
104
  Below is the output of the `--help` or `-h` command. This displays all the available arguments and a brief description of how to use them.
104
105
  ```
105
- usage: twitch-archiver [-h] (-c CHANNEL | -v VOD_ID) [-i CLIENT_ID] [-s CLIENT_SECRET] [-C] [-V]
106
- [-t THREADS] [-q QUALITY] [-d DIRECTORY] [-w] [-L LOG_FILE] [-I CONFIG_DIR]
107
- [-p PUSHBULLET_KEY] [-Q | -D] [--version] [--show-config]
106
+ usage: twitch-archiver [-h] (-c CHANNEL | -v VOD_ID) [-i CLIENT_ID] [-s CLIENT_SECRET] [-C] [-V]
107
+ [-t THREADS] [-q QUALITY] [-d DIRECTORY] [-w] [-l | -a | -R] [-L LOG_FILE]
108
+ [-I CONFIG_DIR] [-p PUSHBULLET_KEY] [-Q | -D] [--version] [--show-config]
108
109
 
109
110
  requires one of:
110
111
  -c CHANNEL, --channel CHANNEL
@@ -141,9 +142,12 @@ optional arguments:
141
142
  -d DIRECTORY, --directory DIRECTORY
142
143
  Directory to store archived VOD(s), use TWO slashes for Windows paths.
143
144
  (default: $CURRENT_DIRECTORY)
144
- -w, --watch Continually check every 10 seconds for new streams from the specified channel.
145
- -S, --stream-only Only download streams which are currently live.
146
- -N, --no-stream Don't download streams which are currently live.
145
+ -w, --watch Continually check every 10 seconds for new streams/VODs from a specified channel.
146
+ -l, --live-only Only download streams / VODs which are currently live.
147
+ -a, --archive-only Don't download streams / VODs which are currently live.
148
+ -R, --real-time-archiver
149
+ Enable real-time stream archiver.
150
+ Read https://github.com/Brisppy/twitch-archiver/wiki/Wiki#real-time-archiver.
147
151
  -L LOG_FILE, --log-file LOG_FILE
148
152
  Output logs to specified file.
149
153
  -I CONFIG_DIR, --config-dir CONFIG_DIR
@@ -26,7 +26,7 @@ Primarily focused on data preservation, this script can be used to archive an en
26
26
  ## Features
27
27
  * Archives both video and chat logs.
28
28
  * VODs can be downloaded as fast as your Internet connection (and storage) can handle.[^1]
29
- * Allows the downloading of live VODs before sections are muted or deleted.[^2]
29
+ * Allows real-time archiving of Twitch streams.[^2]
30
30
  * Generates a readable chat log with timestamps and user badges.
31
31
  * Supports downloading streams which aren't being archived by Twitch.
32
32
  * Error notifications supported with pushbullet.
@@ -54,8 +54,9 @@ Make sure to read the [usage](#usage) section after installation.
54
54
 
55
55
  1. Either download the repository via the green code button at the top of the page, or grab the latest release [here](https://github.com/Brisppy/twitch-archiver/releases/latest).
56
56
  2. Unpack the archive and enter the directory with `cd twitch-archiver`.
57
- 2. Install [pip](https://pip.pypa.io/en/stable/installation/) if you do not already have it.
58
- 3. Build the package with `python -m build`, then install with `python -m pip install ./dist/twitch-archiver-*.tar.gz`.
57
+ 3. Install [pip](https://pip.pypa.io/en/stable/installation/) if you do not already have it.
58
+ 4. Install Python "Build" package with `python -m pip install --upgrade build`.
59
+ 5. Build the package with `python -m build`, then install with `python -m pip install ./dist/twitch-archiver-*.tar.gz`.
59
60
 
60
61
  #### Installing as a Docker Container
61
62
  1. Either download the repository via the green code button at the top of the page, or grab the latest release [here](https://github.com/Brisppy/twitch-archiver/releases/latest).
@@ -88,9 +89,9 @@ Would download VODs **1276315849** and **1275305106** to the directory **/mnt/tw
88
89
  #### Arguments
89
90
  Below is the output of the `--help` or `-h` command. This displays all the available arguments and a brief description of how to use them.
90
91
  ```
91
- usage: twitch-archiver [-h] (-c CHANNEL | -v VOD_ID) [-i CLIENT_ID] [-s CLIENT_SECRET] [-C] [-V]
92
- [-t THREADS] [-q QUALITY] [-d DIRECTORY] [-w] [-L LOG_FILE] [-I CONFIG_DIR]
93
- [-p PUSHBULLET_KEY] [-Q | -D] [--version] [--show-config]
92
+ usage: twitch-archiver [-h] (-c CHANNEL | -v VOD_ID) [-i CLIENT_ID] [-s CLIENT_SECRET] [-C] [-V]
93
+ [-t THREADS] [-q QUALITY] [-d DIRECTORY] [-w] [-l | -a | -R] [-L LOG_FILE]
94
+ [-I CONFIG_DIR] [-p PUSHBULLET_KEY] [-Q | -D] [--version] [--show-config]
94
95
 
95
96
  requires one of:
96
97
  -c CHANNEL, --channel CHANNEL
@@ -127,9 +128,12 @@ optional arguments:
127
128
  -d DIRECTORY, --directory DIRECTORY
128
129
  Directory to store archived VOD(s), use TWO slashes for Windows paths.
129
130
  (default: $CURRENT_DIRECTORY)
130
- -w, --watch Continually check every 10 seconds for new streams from the specified channel.
131
- -S, --stream-only Only download streams which are currently live.
132
- -N, --no-stream Don't download streams which are currently live.
131
+ -w, --watch Continually check every 10 seconds for new streams/VODs from a specified channel.
132
+ -l, --live-only Only download streams / VODs which are currently live.
133
+ -a, --archive-only Don't download streams / VODs which are currently live.
134
+ -R, --real-time-archiver
135
+ Enable real-time stream archiver.
136
+ Read https://github.com/Brisppy/twitch-archiver/wiki/Wiki#real-time-archiver.
133
137
  -L LOG_FILE, --log-file LOG_FILE
134
138
  Output logs to specified file.
135
139
  -I CONFIG_DIR, --config-dir CONFIG_DIR
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "twitch-archiver"
7
- version = "3.0.0.dev1"
7
+ version = "3.0.0.dev5"
8
8
  authors = [
9
9
  { name="Brisppy", email="brisppy@protonmail.com" },
10
10
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: twitch-archiver
3
- Version: 3.0.0.dev1
3
+ Version: 3.0.0.dev5
4
4
  Summary: A simple, fast, platform-independent tool for downloading Twitch streams, videos, and chat logs.
5
5
  Author-email: Brisppy <brisppy@protonmail.com>
6
6
  Project-URL: Homepage, https://github.com/Brisppy/twitch-archiver
@@ -40,7 +40,7 @@ Primarily focused on data preservation, this script can be used to archive an en
40
40
  ## Features
41
41
  * Archives both video and chat logs.
42
42
  * VODs can be downloaded as fast as your Internet connection (and storage) can handle.[^1]
43
- * Allows the downloading of live VODs before sections are muted or deleted.[^2]
43
+ * Allows real-time archiving of Twitch streams.[^2]
44
44
  * Generates a readable chat log with timestamps and user badges.
45
45
  * Supports downloading streams which aren't being archived by Twitch.
46
46
  * Error notifications supported with pushbullet.
@@ -68,8 +68,9 @@ Make sure to read the [usage](#usage) section after installation.
68
68
 
69
69
  1. Either download the repository via the green code button at the top of the page, or grab the latest release [here](https://github.com/Brisppy/twitch-archiver/releases/latest).
70
70
  2. Unpack the archive and enter the directory with `cd twitch-archiver`.
71
- 2. Install [pip](https://pip.pypa.io/en/stable/installation/) if you do not already have it.
72
- 3. Build the package with `python -m build`, then install with `python -m pip install ./dist/twitch-archiver-*.tar.gz`.
71
+ 3. Install [pip](https://pip.pypa.io/en/stable/installation/) if you do not already have it.
72
+ 4. Install Python "Build" package with `python -m pip install --upgrade build`.
73
+ 5. Build the package with `python -m build`, then install with `python -m pip install ./dist/twitch-archiver-*.tar.gz`.
73
74
 
74
75
  #### Installing as a Docker Container
75
76
  1. Either download the repository via the green code button at the top of the page, or grab the latest release [here](https://github.com/Brisppy/twitch-archiver/releases/latest).
@@ -102,9 +103,9 @@ Would download VODs **1276315849** and **1275305106** to the directory **/mnt/tw
102
103
  #### Arguments
103
104
  Below is the output of the `--help` or `-h` command. This displays all the available arguments and a brief description of how to use them.
104
105
  ```
105
- usage: twitch-archiver [-h] (-c CHANNEL | -v VOD_ID) [-i CLIENT_ID] [-s CLIENT_SECRET] [-C] [-V]
106
- [-t THREADS] [-q QUALITY] [-d DIRECTORY] [-w] [-L LOG_FILE] [-I CONFIG_DIR]
107
- [-p PUSHBULLET_KEY] [-Q | -D] [--version] [--show-config]
106
+ usage: twitch-archiver [-h] (-c CHANNEL | -v VOD_ID) [-i CLIENT_ID] [-s CLIENT_SECRET] [-C] [-V]
107
+ [-t THREADS] [-q QUALITY] [-d DIRECTORY] [-w] [-l | -a | -R] [-L LOG_FILE]
108
+ [-I CONFIG_DIR] [-p PUSHBULLET_KEY] [-Q | -D] [--version] [--show-config]
108
109
 
109
110
  requires one of:
110
111
  -c CHANNEL, --channel CHANNEL
@@ -141,9 +142,12 @@ optional arguments:
141
142
  -d DIRECTORY, --directory DIRECTORY
142
143
  Directory to store archived VOD(s), use TWO slashes for Windows paths.
143
144
  (default: $CURRENT_DIRECTORY)
144
- -w, --watch Continually check every 10 seconds for new streams from the specified channel.
145
- -S, --stream-only Only download streams which are currently live.
146
- -N, --no-stream Don't download streams which are currently live.
145
+ -w, --watch Continually check every 10 seconds for new streams/VODs from a specified channel.
146
+ -l, --live-only Only download streams / VODs which are currently live.
147
+ -a, --archive-only Don't download streams / VODs which are currently live.
148
+ -R, --real-time-archiver
149
+ Enable real-time stream archiver.
150
+ Read https://github.com/Brisppy/twitch-archiver/wiki/Wiki#real-time-archiver.
147
151
  -L LOG_FILE, --log-file LOG_FILE
148
152
  Output logs to specified file.
149
153
  -I CONFIG_DIR, --config-dir CONFIG_DIR
@@ -41,9 +41,9 @@ from twitcharchiver.exceptions import TwitchAPIError
41
41
  from twitcharchiver.logger import Logger
42
42
  from twitcharchiver.processing import Processing
43
43
  from twitcharchiver.twitch import Twitch
44
- from twitcharchiver.utils import getenv, send_push, get_latest_version, version_tuple
44
+ from twitcharchiver.utils import getenv, send_push, get_latest_version, version_tuple, check_update_available
45
45
 
46
- __version__ = '3.0.0'
46
+ __version__ = '3.0.0.dev5'
47
47
 
48
48
 
49
49
  def main():
@@ -105,24 +105,28 @@ def main():
105
105
  parser.add_argument('-w', '--watch', action='store_true',
106
106
  help='Continually check every 10 seconds for new streams/VODs from a specified channel.',
107
107
  default=getenv('TWITCH_ARCHIVER_WATCH', False, True))
108
- stream.add_argument('-S', '--stream-only', action='store_true',
109
- default=getenv('TWITCH_ARCHIVER_STREAM_ONLY', False, True),
110
- help='Only download streams which are currently live.')
111
- stream.add_argument('-N', '--no-stream', action='store_true',
112
- help="Don't download streams which are currently live.",
113
- default=getenv("TWITCH_ARCHIVER_NO_STREAM", False, True))
108
+ stream.add_argument('-l', '--live-only', action='store_true',
109
+ default=getenv('TWITCH_ARCHIVER_LIVE_ONLY', False, True),
110
+ help='Only download streams / VODs which are currently live.')
111
+ stream.add_argument('-a', '--archive-only', action='store_true',
112
+ help="Don't download streams / VODs which are currently live.",
113
+ default=getenv("TWITCH_ARCHIVER_ARCHIVE_ONLY", False, True))
114
+ stream.add_argument('-R', '--real-time-archiver', action='store_true',
115
+ help="Enable real-time stream archiver.\n"
116
+ "Read https://github.com/Brisppy/twitch-archiver/wiki/Wiki#real-time-archiver.",
117
+ default=getenv('TWITCH_ARCHIVER_REAL_TIME_ARCHIVER', False, True))
114
118
  parser.add_argument('-L', '--log-file', action='store', help='Output logs to specified file.', type=Path,
115
119
  default=getenv("TWITCH_ARCHIVER_LOG_FILE", False))
116
120
  parser.add_argument('-I', '--config-dir', action='store', type=Path,
117
121
  help='Directory to store configuration, VOD database and lock files.\n(default: %(default)s)',
118
122
  default=getenv('TWITCH_ARCHIVER_CONFIG_DIR',
119
- Path(os.path.expanduser("~"), '.config', 'twitch-archiver')))
123
+ Path(os.path.expanduser("~"), '.config', 'twitch-archiver')))
120
124
  parser.add_argument('-p', '--pushbullet-key', action='store',
121
125
  help='Pushbullet key for sending pushes on error. Enabled by supplying key.',
122
126
  default=getenv("TWITCH_ARCHIVER_PUSHBULLET_KEY", False))
123
127
  loglevel.add_argument('-Q', '--quiet', action='store_const', help='Disable all log output.', const=50, default=0)
124
128
  loglevel.add_argument('-D', '--debug', action='store_const', help='Enable debug logs.', const=10, default=0)
125
- parser.add_argument('--version', action='version', version=f'{__name__} v{__version__}',
129
+ parser.add_argument('--version', action='version', version=f'Twitch Archiver v{__version__}',
126
130
  help='Show version number and exit.')
127
131
  parser.add_argument('--show-config', action='store_true', help='Show saved config and exit.', default=False)
128
132
 
@@ -144,7 +148,7 @@ def main():
144
148
 
145
149
  # compare with current git version
146
150
  latest_version, release_notes = get_latest_version()
147
- if version_tuple(__version__) < version_tuple(latest_version):
151
+ if check_update_available(__version__, latest_version):
148
152
  log.warning('New version of Twitch-Archiver available - Version %s:\n'
149
153
  'https://github.com/Brisppy/twitch-archiver/releases/latest\nRelease notes:\n\n%s\n',
150
154
  latest_version, release_notes)
@@ -177,7 +181,7 @@ def main():
177
181
  # store returned token
178
182
  config.save(Path(args.get('config_dir'), 'config.ini'))
179
183
  except TwitchAPIError as err:
180
- log.error('OAuth token generation failed. Error: $s', str(err))
184
+ log.error('OAuth token generation failed. Error: %s', str(err))
181
185
  send_push(config.get('pushbullet_key'), 'OAuth token generation failed.', str(err))
182
186
  sys.exit(1)
183
187
 
@@ -33,7 +33,7 @@ class Arguments:
33
33
 
34
34
  # validate mutual exclusivity of arguments passed via CLI and environment variables
35
35
  # required as values set via environment variables bypass argparse mutex handling
36
- for mutex_args in (("vod_id", "channel"), ("stream_only", "no_stream")):
36
+ for mutex_args in (("vod_id", "channel"), ("live_only", "archive_only")):
37
37
  mutex_arg_0, mutex_arg_1 = Arguments.get(mutex_args[0]), Arguments.get(mutex_args[1])
38
38
  # check if both mutex args have a value (including empty string)
39
39
  if mutex_arg_0 is not None and mutex_arg_1 is not None:
@@ -45,8 +45,8 @@ class Processing:
45
45
  self.video = args['video']
46
46
  self.chat = args['chat']
47
47
  self.quality = args['quality']
48
- self.stream_only = args['stream_only']
49
- self.no_stream = args['no_stream']
48
+ self.live_only = args['live_only']
49
+ self.archive_only = args['archive_only']
50
50
  self.config_dir = args['config_dir']
51
51
  self.quiet = args['quiet']
52
52
  self.debug = args['debug']
@@ -109,10 +109,11 @@ class Processing:
109
109
  stream_length = time_since_date(datetime.strptime(
110
110
  channel_data[0]['started_at'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).timestamp())
111
111
 
112
+ # TODO: ensure that if the stream ends, we clean up and merge segments
112
113
  # while we wait for the api to update we must build a temporary buffer of any parts advertised in the
113
114
  # meantime in case there is no vod and thus no way to retrieve them after the fact
114
- if stream_length < 60:
115
- self.log.debug('Stream began less than 60s ago, delaying archival start until VOD API updated.')
115
+ if stream_length < 90:
116
+ self.log.debug('Stream began less than 90s ago, delaying archival start until VOD API updated.')
116
117
  # create temp dir for buffer
117
118
  Path(tmp_buffer_dir).mkdir(parents=True, exist_ok=True)
118
119
 
@@ -120,7 +121,7 @@ class Processing:
120
121
  index_uri = self.call_twitch.get_channel_hls_index(channel, self.quality)
121
122
 
122
123
  # download new parts every 4s
123
- for i in range(int((60 - stream_length) / 4)):
124
+ for i in range(int((90 - stream_length) / 4)):
124
125
  # grab required values
125
126
  start_timestamp = int(datetime.utcnow().timestamp())
126
127
  incoming_segments = m3u8.loads(Api.get_request(index_uri).text).data
@@ -135,7 +136,7 @@ class Processing:
135
136
  sleep(4 - processing_time)
136
137
 
137
138
  # wait any remaining time
138
- sleep((60 - stream_length) % 4)
139
+ sleep((90 - stream_length) % 4)
139
140
 
140
141
  # retrieve available vods
141
142
  available_vods: dict[int: tuple[int]] = {}
@@ -154,8 +155,7 @@ class Processing:
154
155
  self.log.error('Error retrieving VODs from Twitch. Error: %s', str(e))
155
156
  continue
156
157
 
157
- self.log.info(f'Online VODs: {available_vods}' if self.debug
158
- else f'Online VODs: {len(available_vods)}')
158
+ self.log.debug(f'Online VODs: {available_vods}')
159
159
 
160
160
  # retrieve downloaded vods
161
161
  with Database(Path(self.config_dir, 'vods.db')) as db:
@@ -163,8 +163,7 @@ class Processing:
163
163
  downloaded_vods = dict([(i[0], (i[1], i[2], i[3])) for i in db.execute_query(
164
164
  'SELECT stream_id, vod_id, video_archived, chat_archived FROM vods WHERE user_id IS ?',
165
165
  {'user_id': user_id})])
166
- self.log.info(f'Downloaded vods: {downloaded_vods}' if self.debug
167
- else f'Downloaded vods: {len(downloaded_vods)}')
166
+ self.log.debug(f'Downloaded vods: {downloaded_vods}')
168
167
 
169
168
  # generate vod queue using downloaded and available vods
170
169
  vod_queue = {}
@@ -190,15 +189,18 @@ class Processing:
190
189
  live_vod_exists = (channel_data and int(channel_data[0]['id']) in available_vods.keys())
191
190
 
192
191
  # move on if channel offline and no vods are available
193
- if not self.stream_only and not channel_live and not available_vods:
192
+ if not self.live_only and not channel_live and not available_vods:
194
193
  self.log.info('No VODs were found for %s.', user_name)
195
194
  continue
196
195
 
197
- if not channel_live and self.stream_only:
196
+ # move on if channel offline and we are only looking for live vods
197
+ if not channel_live and self.live_only:
198
+ self.log.info('Running in stream-only mode and no stream available for %s.', user_name)
198
199
  continue
199
200
 
200
- # archive stream in non-segmented mode if no paired vod exists
201
- if not self.no_stream and channel_live and not live_vod_exists and self.video:
201
+ # archive stream in non-segmented mode if no paired vod exists, unless we are in no_stream mode or not
202
+ # archiving video.
203
+ if not self.archive_only and channel_live and not live_vod_exists and self.video:
202
204
  self.log.info('Channel live but not being archived to a VOD, running stream archiver.')
203
205
  self.log.debug('Creating lock file for stream.')
204
206
 
@@ -214,7 +216,7 @@ class Processing:
214
216
 
215
217
  # Check if stream id in database
216
218
  if channel_data[0]['id'] in downloaded_streams:
217
- self.log.info('Stream has already been downloaded.')
219
+ self.log.info('Ignoring steam as it has already been downloaded.')
218
220
 
219
221
  else:
220
222
  try:
@@ -271,11 +273,13 @@ class Processing:
271
273
  # begin processing each available vod
272
274
  for stream_id in vod_queue:
273
275
  vod_id = vod_queue[stream_id][0]
274
- # skip if we are only after the current stream
275
- if channel_data and self.stream_only and stream_id != int(channel_data[0]['id']):
276
+
277
+ # skip if we are only after currently live streams, and stream_id is not live
278
+ if channel_data and self.live_only and stream_id != int(channel_data[0]['id']):
276
279
  continue
277
280
 
278
- if channel_data and self.no_stream and stream_id == int(channel_data[0]['id']):
281
+ # skip if we aren't after currently lives streams, and stream_id is live
282
+ if channel_data and self.archive_only and stream_id == int(channel_data[0]['id']):
279
283
  self.log.info('Skipping VOD as it is live and no-stream argument provided.')
280
284
  continue
281
285
 
@@ -432,7 +436,8 @@ class Processing:
432
436
  _r = None
433
437
 
434
438
  try:
435
- if vod_live:
439
+ # begin real-time archiver if VOD still live and real-time archiver enabled
440
+ if self.real_time_archiver and vod_live:
436
441
  stream = Stream(self.client_id, self.client_secret, self.oauth_token)
437
442
  # concurrently grab live pieces and vod chunks
438
443
 
@@ -82,14 +82,25 @@ class Stream:
82
82
  while True:
83
83
  start_timestamp = int(datetime.utcnow().timestamp())
84
84
 
85
- try:
86
- self.log.debug('Fetching incoming stream segments.')
87
- incoming_segments = m3u8.loads(Api.get_request(index_uri).text).data
85
+ # attempt to grab new segments from Twitch
86
+ for attempt in range(6):
87
+ if attempt > 4:
88
+ return
88
89
 
89
- # stream has ended if exception encountered
90
- except TwitchAPIErrorNotFound:
91
- self.get_final_segment(self.buffer, output_dir, self.segment_ids)
92
- return
90
+ try:
91
+ self.log.debug('Fetching incoming stream segments.')
92
+ incoming_segments = m3u8.loads(Api.get_request(index_uri).text).data
93
+ break
94
+
95
+ # stream has ended if exception encountered
96
+ except TwitchAPIErrorNotFound:
97
+ self.get_final_segment(self.buffer, output_dir, self.segment_ids)
98
+ return
99
+
100
+ except requests.exceptions.ConnectTimeout:
101
+ self.log.debug('Timed out attempting to fetch new stream segments, retrying. (Attempt %s)',
102
+ attempt + 1)
103
+ continue
93
104
 
94
105
  # set latest segment and timestamp if new segment found
95
106
  if incoming_segments['segments'][-1]['uri'] != latest_segment:
@@ -41,7 +41,7 @@ def generate_readable_chat_log(chat_log, stream_start):
41
41
  created_time = \
42
42
  datetime.strptime(comment['createdAt'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
43
43
 
44
- comment_time = '{:.3f}'.format(get_time_difference(stream_start, created_time))
44
+ comment_time = f'{get_time_difference(stream_start, created_time):.3f}'
45
45
 
46
46
  # catch comments without commenter informations
47
47
  if comment['commenter']:
@@ -107,7 +107,7 @@ def export_json(vod_json):
107
107
 
108
108
  :param vod_json: dict of vod parameters retrieved from twitch
109
109
  """
110
- with open(Path(vod_json['store_directory'], 'vod.json'), 'w') as json_out_file:
110
+ with open(Path(vod_json['store_directory'], 'vod.json'), 'w', encoding='utf8') as json_out_file:
111
111
  json_out_file.write(json.dumps(vod_json))
112
112
 
113
113
 
@@ -117,10 +117,10 @@ def import_json(vod_json):
117
117
  :param vod_json: dict of vod parameters retrieved from twitch
118
118
  """
119
119
  if Path(vod_json['store_directory'], 'vod.json').exists():
120
- with open(Path(vod_json['store_directory'], 'vod.json'), 'r') as json_in_file:
120
+ with open(Path(vod_json['store_directory'], 'vod.json'), 'r', encoding='utf8') as json_in_file:
121
121
  return json.loads(json_in_file.read())
122
122
 
123
- return
123
+ return []
124
124
 
125
125
 
126
126
  def combine_vod_parts(vod_json, print_progress=True):
@@ -158,10 +158,10 @@ def combine_vod_parts(vod_json, print_progress=True):
158
158
  # merge all .ts files with ffmpeg concat demuxer as missing segments can cause corruption with
159
159
  # other method
160
160
 
161
- log.debug(f'Discontinuity found, merging with ffmpeg.\n Discontinuity: {dicontinuity}')
161
+ log.debug('Discontinuity found, merging with ffmpeg.\n Discontinuity: %s', dicontinuity)
162
162
 
163
163
  # create file with list of parts for ffmpeg
164
- with open(Path(vod_json['store_directory'], 'parts', 'segments.txt'), mode='w') as segment_file:
164
+ with open(Path(vod_json['store_directory'], 'parts', 'segments.txt'), 'w', encoding='utf8') as segment_file:
165
165
  for part in vod_parts:
166
166
  segment_file.write(f"file '{part}'\n")
167
167
 
@@ -181,7 +181,7 @@ def combine_vod_parts(vod_json, print_progress=True):
181
181
  progress.print_progress(int(current_time), vod_json['duration'])
182
182
 
183
183
  if p.returncode:
184
- log.error(f'VOD merger exited with error. Command: {p.args}.')
184
+ log.error('VOD merger exited with error. Command: %s.', p.args)
185
185
  raise VodConvertError(f'VOD merger exited with error. Command: {p.args}.')
186
186
 
187
187
 
@@ -204,9 +204,9 @@ def convert_vod(vod_json, ignore_corruptions=None, print_progress=True):
204
204
  [corrupt_part_whitelist.update(r) for r in [range(t[0] - 2, t[1] + 3) for t in ignore_corruptions]]
205
205
 
206
206
  # get dts offset of first part
207
- with subprocess.Popen(
208
- f'ffprobe -v quiet -print_format json -show_format -show_streams "{Path(vod_json["store_directory"], "parts", "00000.ts")}"', shell=True,
209
- stdout=subprocess.PIPE, universal_newlines=True) as p:
207
+ with subprocess.Popen(f'ffprobe -v quiet -print_format json -show_format -show_streams '
208
+ f'"{Path(vod_json["store_directory"], "parts", "00000.ts")}"', shell=True,
209
+ stdout=subprocess.PIPE, universal_newlines=True) as p:
210
210
  ts_file_data = ''
211
211
  for line in p.stdout:
212
212
  ts_file_data += line
@@ -240,27 +240,26 @@ def convert_vod(vod_json, ignore_corruptions=None, print_progress=True):
240
240
  dts_timestamp = int(re.search(r'(?<=dts = ).*(?=\).)', line).group(0))
241
241
 
242
242
  # Catch corrupt parts without timestamp, shows up as 'NOPTS'
243
- except ValueError:
243
+ except ValueError as e:
244
244
  raise VodConvertError("Corrupt packet encountered at unknown timestamp while converting VOD. "
245
- "Delete 'parts' folder and re-download VOD.")
245
+ "Delete 'parts' folder and re-download VOD.") from e
246
246
 
247
247
  corrupt_part = floor((dts_timestamp - dts_offset) / 90000 / 10)
248
248
 
249
249
  # ignore if corrupt packet within ignore_corruptions range
250
250
  if corrupt_part in corrupt_part_whitelist:
251
- log.debug(f'Ignoring corrupt packet as part in whitelist. Part: {corrupt_part}')
252
- pass
251
+ log.debug('Ignoring corrupt packet as part in whitelist. Part: %s', corrupt_part)
253
252
 
254
253
  else:
255
254
  corrupt_parts.add(int(corrupt_part))
256
- log.error(f'Corrupt packet encountered. Part: {corrupt_part}')
255
+ log.error('Corrupt packet encountered. Part: %s', corrupt_part)
257
256
 
258
257
  if p.returncode:
259
258
  log.debug('FFmpeg exited with error code, output dumped to VOD directory.')
260
- with open(Path(vod_json["store_directory"], 'parts', 'ffmpeg.log'), 'w') as ffout:
259
+ with open(Path(vod_json["store_directory"], 'parts', 'ffmpeg.log'), 'w', encoding='utf8') as ffout:
261
260
  ffout.write(ffmpeg_log)
262
261
 
263
- raise VodConvertError(f"VOD converter exited with error. Delete 'parts' directory and re-download VOD.")
262
+ raise VodConvertError("VOD converter exited with error. Delete 'parts' directory and re-download VOD.")
264
263
 
265
264
  if corrupt_parts:
266
265
  corrupted_ranges = to_ranges(corrupt_parts)
@@ -277,6 +276,11 @@ def convert_vod(vod_json, ignore_corruptions=None, print_progress=True):
277
276
 
278
277
  # https://stackoverflow.com/a/43091576
279
278
  def to_ranges(iterable):
279
+ """Converts a list of integers to iterable sets of (low, high) (e.g [0, 1, 2, 5, 7, 8] -> (0, 2), (5, 5), (7, 8))
280
+
281
+ :param iterable: list of integers
282
+ :return: iterable generator of separate integer ranges
283
+ """
280
284
  iterable = sorted(set(iterable))
281
285
  for key, group in groupby(enumerate(iterable), lambda t: t[1] - t[0]):
282
286
  group = list(group)
@@ -302,25 +306,24 @@ def verify_vod_length(vod_json):
302
306
  shell=True, capture_output=True)
303
307
 
304
308
  if p.returncode:
305
- log.error(f'VOD length verification exited with error. Command: {p.args}.')
309
+ log.error('VOD length verification exited with error. Command: %s.', p.args)
306
310
  raise VodConvertError(f'VOD length verification exited with error. Command: {p.args}.')
307
311
 
308
312
  try:
309
313
  downloaded_length = int(float(p.stdout.decode('ascii').rstrip()))
310
314
 
311
315
  except Exception as e:
312
- log.error(f'Failed to fetch downloaded VOD length. VOD may not have downloaded correctly. {e}')
313
- raise VodConvertError(str(e))
316
+ log.error('Failed to fetch downloaded VOD length. VOD may not have downloaded correctly. %s', str(e))
317
+ raise VodConvertError(str(e)) from e
314
318
 
315
- log.debug(f'Downloaded VOD length is {downloaded_length}. Expected length is {vod_json["duration"]}.')
319
+ log.debug('Downloaded VOD length is %s. Expected length is %s.', downloaded_length, vod_json["duration"])
316
320
 
317
321
  # pass verification if downloaded file is within 2s of expected length
318
322
  if 2 >= downloaded_length - vod_json['duration'] >= -2:
319
323
  log.debug('VOD passed length verification.')
320
324
  return False
321
325
 
322
- else:
323
- return True
326
+ return True
324
327
 
325
328
 
326
329
  def cleanup_vod_parts(vod_directory):
@@ -364,10 +367,10 @@ def convert_to_seconds(duration):
364
367
  if len(duration) == 1:
365
368
  return int(duration[0])
366
369
 
367
- elif len(duration) == 2:
370
+ if len(duration) == 2:
368
371
  return (int(duration[0]) * 60) + int(duration[1])
369
372
 
370
- elif len(duration) == 3:
373
+ if len(duration) == 3:
371
374
  return (int(duration[0]) * 3600) + (int(duration[1]) * 60) + int(duration[2])
372
375
 
373
376
 
@@ -380,7 +383,7 @@ def convert_to_hms(seconds):
380
383
  minutes = seconds // 60
381
384
  hours = minutes // 60
382
385
 
383
- return "%02dh%02dm%02ds" % (hours, minutes % 60, seconds % 60)
386
+ return f"{hours:02d}h{minutes % 60:02d}m{seconds % 60:02d}s"
384
387
 
385
388
 
386
389
  def create_lock(ini_path, vod_id):
@@ -391,8 +394,9 @@ def create_lock(ini_path, vod_id):
391
394
  :return: true if lock file creation fails
392
395
  """
393
396
  try:
394
- with open(Path(ini_path, f'.lock.{vod_id}'), 'x') as _:
397
+ with open(Path(ini_path, f'.lock.{vod_id}'), 'x', encoding='utf8') as _:
395
398
  pass
399
+ return
396
400
 
397
401
  except FileExistsError:
398
402
  return True
@@ -407,6 +411,7 @@ def remove_lock(config_dir, vod_id):
407
411
  """
408
412
  try:
409
413
  Path(config_dir, f'.lock.{vod_id}').unlink()
414
+ return
410
415
 
411
416
  except Exception as e:
412
417
  return e
@@ -459,9 +464,38 @@ def get_latest_version():
459
464
  # reference:
460
465
  # https://stackoverflow.com/a/11887825
461
466
  def version_tuple(v):
467
+ """Convert
468
+
469
+ :param v:
470
+ :return:
471
+ """
462
472
  return tuple(map(int, (v.split("."))))
463
473
 
464
474
 
475
+ def check_update_available(local_version, remote_version):
476
+ """Compares two software versions.
477
+
478
+ :param local_version: local version in use
479
+ :param remote_version: remote version to compare against
480
+ :return: True if remote version has a higher version number, otherwise False
481
+ """
482
+ # check if local version is 'special', as in a development build or release candidate
483
+ local_version_parts = local_version.split('.')
484
+ if len(local_version_parts) > 3:
485
+ log.warning(
486
+ 'Currently using a development or release candidate build. These may be unfinished or contain serious '
487
+ 'bugs. Report any issues you encounter to https://github.com/Brisppy/twitch-archiver/issues.')
488
+ # update is available if we are using a dev or release candidate build equal to or prior to
489
+ # the latest stable release
490
+ if version_tuple('.'.join(local_version_parts[:-1])) <= version_tuple(remote_version):
491
+ return True
492
+
493
+ elif version_tuple(local_version) < version_tuple(remote_version):
494
+ return True
495
+
496
+ return False
497
+
498
+
465
499
  def get_quality_index(desired_quality, available_qualities):
466
500
  """Finds the index of a user defined quality from a list of available stream qualities.
467
501
 
@@ -509,13 +543,13 @@ def send_push(pushbullet_key, title, body=''):
509
543
  if _r.status_code != 200:
510
544
  if _r.json()['error']['code'] == 'pushbullet_pro_required':
511
545
  log.error('Error sending push. Likely rate limited (500/month). '
512
- f'Error {_r.status_code}: {_r.text}')
546
+ 'Error %s: %s', _r.status_code, _r.text)
513
547
 
514
548
  else:
515
- log.error(f'Error sending push. Error {_r.status_code}: {_r.text}')
549
+ log.error('Error sending push. Error %s: %s', _r.status_code, _r.text)
516
550
 
517
551
  except Exception as e:
518
- log.error(f'Error sending push. Error: {e}')
552
+ log.error('Error sending push. Error: %s', e)
519
553
 
520
554
 
521
555
  # reference:
@@ -586,13 +620,14 @@ def getenv(name, default_val=None, is_bool=False):
586
620
  if is_bool and isinstance(val, str):
587
621
  if val.upper() == "TRUE":
588
622
  return True
589
- elif val.upper() == "FALSE":
623
+
624
+ if val.upper() == "FALSE":
590
625
  return False
591
- else:
592
- raise ValueError(f"Invalid boolean value (true or false) received for environment variable: {name}={val}")
593
- else:
594
- # return empty strings '' as None type
595
- return val if val else None
626
+
627
+ raise ValueError(f"Invalid boolean value (true or false) received for environment variable: {name}={val}")
628
+
629
+ # return empty strings '' as None type
630
+ return val if val else None
596
631
 
597
632
 
598
633
  def format_vod_chapters(chapters):
@@ -611,7 +646,7 @@ def format_vod_chapters(chapters):
611
646
 
612
647
  """)
613
648
 
614
- if type(chapters) == tuple:
649
+ if isinstance(chapters, tuple):
615
650
  formatted_chapters += chapter_base.format(
616
651
  start=chapters[1],
617
652
  end=chapters[2],
@@ -652,7 +687,7 @@ class Progress:
652
687
  """
653
688
  m, s = divmod(s, 60)
654
689
  h, m = divmod(m, 60)
655
- return '{:0>2}:{:0>2}:{:0>2}'.format(h, m, s)
690
+ return f'{h:0>2}:{m:0>2}:{s:0>2}'
656
691
 
657
692
  def print_progress(self, cur, total, last_frame=False):
658
693
  """Prints and updates a nice progress bar.