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.
- {twitch-archiver-3.0.0.dev1/twitch_archiver.egg-info → twitch-archiver-3.0.0.dev5}/PKG-INFO +14 -10
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/README.md +13 -9
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/pyproject.toml +1 -1
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5/twitch_archiver.egg-info}/PKG-INFO +14 -10
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/__init__.py +16 -12
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/arguments.py +1 -1
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/processing.py +24 -19
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/stream.py +18 -7
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/utils.py +73 -38
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/LICENSE +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/setup.cfg +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/setup.py +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/SOURCES.txt +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/dependency_links.txt +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/entry_points.txt +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/top_level.txt +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/api.py +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/configuration.py +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/database.py +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/downloader.py +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/exceptions.py +0 -0
- {twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitcharchiver/logger.py +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
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] [-
|
|
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
|
|
145
|
-
-
|
|
146
|
-
-
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
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] [-
|
|
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
|
|
131
|
-
-
|
|
132
|
-
-
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: twitch-archiver
|
|
3
|
-
Version: 3.0.0.
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
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] [-
|
|
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
|
|
145
|
-
-
|
|
146
|
-
-
|
|
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('-
|
|
109
|
-
default=getenv('
|
|
110
|
-
help='Only download streams which are currently live.')
|
|
111
|
-
stream.add_argument('-
|
|
112
|
-
help="Don't download streams which are currently live.",
|
|
113
|
-
default=getenv("
|
|
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
|
-
|
|
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'
|
|
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
|
|
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:
|
|
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"), ("
|
|
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.
|
|
49
|
-
self.
|
|
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 <
|
|
115
|
-
self.log.debug('Stream began less than
|
|
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((
|
|
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((
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
275
|
-
if
|
|
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
|
|
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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
# attempt to grab new segments from Twitch
|
|
86
|
+
for attempt in range(6):
|
|
87
|
+
if attempt > 4:
|
|
88
|
+
return
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 = '{
|
|
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(
|
|
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'),
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
370
|
+
if len(duration) == 2:
|
|
368
371
|
return (int(duration[0]) * 60) + int(duration[1])
|
|
369
372
|
|
|
370
|
-
|
|
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 "
|
|
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
|
-
|
|
546
|
+
'Error %s: %s', _r.status_code, _r.text)
|
|
513
547
|
|
|
514
548
|
else:
|
|
515
|
-
log.error(
|
|
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(
|
|
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
|
-
|
|
623
|
+
|
|
624
|
+
if val.upper() == "FALSE":
|
|
590
625
|
return False
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
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}'
|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{twitch-archiver-3.0.0.dev1 → twitch-archiver-3.0.0.dev5}/twitch_archiver.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|