talklib 1.4.0__tar.gz → 2.0.2__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.
- {talklib-1.4.0/src/talklib.egg-info → talklib-2.0.2}/PKG-INFO +35 -34
- {talklib-1.4.0 → talklib-2.0.2}/README.md +12 -33
- {talklib-1.4.0 → talklib-2.0.2}/pyproject.toml +1 -1
- {talklib-1.4.0 → talklib-2.0.2}/requirements.txt +22 -0
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib/__init__.py +2 -1
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib/ev.py +1 -0
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib/ffmpeg.py +6 -1
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib/notify.py +8 -4
- talklib-2.0.2/src/talklib/pod.py +381 -0
- {talklib-1.4.0 → talklib-2.0.2/src/talklib.egg-info}/PKG-INFO +35 -34
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib.egg-info/SOURCES.txt +1 -0
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib.egg-info/requires.txt +22 -0
- {talklib-1.4.0 → talklib-2.0.2}/LICENSE.txt +0 -0
- {talklib-1.4.0 → talklib-2.0.2}/setup.cfg +0 -0
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib/show.py +0 -0
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib/utils.py +0 -0
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib.egg-info/dependency_links.txt +0 -0
- {talklib-1.4.0 → talklib-2.0.2}/src/talklib.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: talklib
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: A package to automate processing of shows/segments airing on the TL
|
|
5
5
|
Author-email: Ben Weddle <ben.weddle@gmail.com>
|
|
6
6
|
Maintainer-email: Ben Weddle <ben.weddle@gmail.com>
|
|
@@ -28,25 +28,37 @@ Project-URL: Issues, https://github.com/talkinglibrary/talklib/issues
|
|
|
28
28
|
Requires-Python: >=3.10
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
License-File: LICENSE.txt
|
|
31
|
+
Requires-Dist: aiohappyeyeballs==2.4.0
|
|
31
32
|
Requires-Dist: aiohttp==3.10.5
|
|
32
33
|
Requires-Dist: aiohttp-retry==2.8.3
|
|
33
34
|
Requires-Dist: aiosignal==1.3.1
|
|
35
|
+
Requires-Dist: annotated-types==0.7.0
|
|
34
36
|
Requires-Dist: async-timeout==4.0.2
|
|
35
37
|
Requires-Dist: attrs==22.1.0
|
|
38
|
+
Requires-Dist: bcrypt==4.2.0
|
|
39
|
+
Requires-Dist: boto3==1.35.18
|
|
40
|
+
Requires-Dist: botocore==1.35.18
|
|
36
41
|
Requires-Dist: build==1.0.3
|
|
37
42
|
Requires-Dist: certifi==2024.8.30
|
|
43
|
+
Requires-Dist: cffi==1.17.1
|
|
38
44
|
Requires-Dist: charset-normalizer==2.1.1
|
|
39
45
|
Requires-Dist: colorama==0.4.6
|
|
40
46
|
Requires-Dist: coverage==6.5.0
|
|
47
|
+
Requires-Dist: cryptography==43.0.3
|
|
48
|
+
Requires-Dist: decorator==5.1.1
|
|
49
|
+
Requires-Dist: Deprecated==1.2.14
|
|
41
50
|
Requires-Dist: docutils==0.20.1
|
|
42
51
|
Requires-Dist: exceptiongroup==1.0.4
|
|
52
|
+
Requires-Dist: fabric==3.2.2
|
|
43
53
|
Requires-Dist: ffmpeg-python==0.2.0
|
|
44
54
|
Requires-Dist: frozenlist==1.4.1
|
|
45
55
|
Requires-Dist: future==0.18.3
|
|
46
56
|
Requires-Dist: idna==3.4
|
|
47
57
|
Requires-Dist: importlib-metadata==7.0.1
|
|
48
58
|
Requires-Dist: iniconfig==1.1.1
|
|
59
|
+
Requires-Dist: invoke==2.2.0
|
|
49
60
|
Requires-Dist: jaraco.classes==3.3.0
|
|
61
|
+
Requires-Dist: jmespath==1.0.1
|
|
50
62
|
Requires-Dist: keyring==24.3.0
|
|
51
63
|
Requires-Dist: markdown-it-py==3.0.0
|
|
52
64
|
Requires-Dist: mdurl==0.1.2
|
|
@@ -54,14 +66,20 @@ Requires-Dist: more-itertools==10.2.0
|
|
|
54
66
|
Requires-Dist: multidict==6.0.4
|
|
55
67
|
Requires-Dist: nh3==0.2.15
|
|
56
68
|
Requires-Dist: packaging==21.3
|
|
69
|
+
Requires-Dist: paramiko==3.5.0
|
|
57
70
|
Requires-Dist: pkginfo==1.9.6
|
|
58
71
|
Requires-Dist: pluggy==1.0.0
|
|
72
|
+
Requires-Dist: pycparser==2.22
|
|
73
|
+
Requires-Dist: pydantic==2.9.1
|
|
74
|
+
Requires-Dist: pydantic_core==2.23.3
|
|
59
75
|
Requires-Dist: Pygments==2.17.2
|
|
60
76
|
Requires-Dist: PyJWT==2.6.0
|
|
77
|
+
Requires-Dist: PyNaCl==1.5.0
|
|
61
78
|
Requires-Dist: pyparsing==3.0.9
|
|
62
79
|
Requires-Dist: pyproject_hooks==1.0.0
|
|
63
80
|
Requires-Dist: pytest==7.2.0
|
|
64
81
|
Requires-Dist: pytest-cov==4.0.0
|
|
82
|
+
Requires-Dist: python-dateutil==2.9.0.post0
|
|
65
83
|
Requires-Dist: pytz==2022.6
|
|
66
84
|
Requires-Dist: pywin32-ctypes==0.2.2
|
|
67
85
|
Requires-Dist: readme-renderer==42.0
|
|
@@ -69,10 +87,14 @@ Requires-Dist: requests==2.32.3
|
|
|
69
87
|
Requires-Dist: requests-toolbelt==1.0.0
|
|
70
88
|
Requires-Dist: rfc3986==2.0.0
|
|
71
89
|
Requires-Dist: rich==13.7.0
|
|
90
|
+
Requires-Dist: s3transfer==0.10.2
|
|
91
|
+
Requires-Dist: six==1.16.0
|
|
72
92
|
Requires-Dist: tomli==2.0.1
|
|
73
93
|
Requires-Dist: twilio==9.0.2
|
|
74
94
|
Requires-Dist: twine==4.0.2
|
|
95
|
+
Requires-Dist: typing_extensions==4.12.2
|
|
75
96
|
Requires-Dist: urllib3==2.2.2
|
|
97
|
+
Requires-Dist: wrapt==1.16.0
|
|
76
98
|
Requires-Dist: yarl==1.9.2
|
|
77
99
|
Requires-Dist: zipp==3.17.0
|
|
78
100
|
|
|
@@ -82,7 +104,7 @@ Requires-Dist: zipp==3.17.0
|
|
|
82
104
|
[](https://github.com/Nashville-Public-Library/talklib/issues)
|
|
83
105
|
[](https://github.com/Nashville-Public-Library/talklib/commits/master)
|
|
84
106
|
|
|
85
|
-
## A package to automate processing
|
|
107
|
+
## A package to automate processing TL shows/segments and podcasts
|
|
86
108
|
|
|
87
109
|
[Skip to Examples](#examples)
|
|
88
110
|
|
|
@@ -102,7 +124,9 @@ Use this module to process the following types of shows/segments:
|
|
|
102
124
|
## Requirements
|
|
103
125
|
|
|
104
126
|
### -[Python](https://www.python.org/downloads/)
|
|
105
|
-
Use Python 3.10 or higher
|
|
127
|
+
Use Python 3.10 or higher.
|
|
128
|
+
|
|
129
|
+
Make sure to select "add to PATH" during installation.
|
|
106
130
|
|
|
107
131
|
### -[FFmpeg](https://www.ffmpeg.org/download.html#build-windows)
|
|
108
132
|
You need Windows binaries for both FFmpeg & FFprobe installed on the PC and added to the PATH:
|
|
@@ -121,11 +145,13 @@ See [below](#disable-twilio) for how to disable Twilio.
|
|
|
121
145
|
### -Environment Variables
|
|
122
146
|
This package uses Environment Variables to help with portability and keep sensitive info separated. The entire list of these is in the `ev.py` file. Make sure to set **all** of these on your PC(s). They are case-sensitive!
|
|
123
147
|
|
|
124
|
-
**ONCE YOU SET/CHANGE/UPDATE ENVIRONMENT VARIABLES ON YOUR PC, YOU NEED TO RESTART WIREREADY**
|
|
148
|
+
> **ONCE YOU SET/CHANGE/UPDATE ENVIRONMENT VARIABLES ON YOUR PC, YOU NEED TO RESTART WIREREADY**
|
|
125
149
|
|
|
126
150
|
---
|
|
127
151
|
## Installation
|
|
128
152
|
|
|
153
|
+
AFTER you have all of the above [requirements](#requirements), inst
|
|
154
|
+
|
|
129
155
|
- Open a terminal/command prompt
|
|
130
156
|
- ````bash
|
|
131
157
|
pip install talklib
|
|
@@ -147,7 +173,10 @@ Before we begin, a general note:
|
|
|
147
173
|
- Instead, we tell WR to run a Batch script (`.bat` file) which in turn will run the Python script (`.py` file).
|
|
148
174
|
- Ensure the Batch & Python scripts are in the same directory.
|
|
149
175
|
- A sample `.bat` file (`Example.bat`) is included in the [misc](https://github.com/Nashville-Public-Library/misc/tree/main/talklib_examples) repo.
|
|
150
|
-
-
|
|
176
|
+
- Download this file and place it in the same folder as your Python file.
|
|
177
|
+
- Right-Click the file and click `Unblock` so that it can be executed.
|
|
178
|
+
- It is a best practice to give the `.bat` and `.py` files the same name, though it is not necessary.
|
|
179
|
+
- PLEASE NOTE: the `.bat` file will run **all** Python files in the folder. This is one reason it is best to separate your Python files into different folders, each with its own `.bat` file.
|
|
151
180
|
|
|
152
181
|
Here is what an example directory structure should look like:
|
|
153
182
|
````
|
|
@@ -156,7 +185,7 @@ D:\wireready
|
|
|
156
185
|
-WP.bat
|
|
157
186
|
-WP.py
|
|
158
187
|
````
|
|
159
|
-
You would
|
|
188
|
+
You would schedule WR to run the `WP.bat` file, which would run the `WP.py` file.
|
|
160
189
|
|
|
161
190
|
----
|
|
162
191
|
|
|
@@ -318,34 +347,6 @@ optional
|
|
|
318
347
|
- be careful with this!
|
|
319
348
|
- default is 21
|
|
320
349
|
|
|
321
|
-
### Notes about formatting:
|
|
322
|
-
|
|
323
|
-
if you're new to Python, here're some reminders
|
|
324
|
-
|
|
325
|
-
### string
|
|
326
|
-
- must be enclosed in quotes (single or double; Python doesn't care)
|
|
327
|
-
|
|
328
|
-
### boolean
|
|
329
|
-
- either True or False
|
|
330
|
-
- do not enclose in quotes
|
|
331
|
-
- must be capitalized
|
|
332
|
-
- correct: True
|
|
333
|
-
- incorrect: true
|
|
334
|
-
|
|
335
|
-
### number
|
|
336
|
-
- in our case, these can be either type int OR float, meaning either whole numbers OR decimal numbers are allowed
|
|
337
|
-
- OK: 5
|
|
338
|
-
- also OK: 5.4
|
|
339
|
-
- do not enclose in quotes
|
|
340
|
-
|
|
341
|
-
## Methods
|
|
342
|
-
|
|
343
|
-
`run()`
|
|
344
|
-
|
|
345
|
-
required
|
|
346
|
-
- executes the script with the attributes you set
|
|
347
|
-
- should be the last line in your script
|
|
348
|
-
|
|
349
350
|
## Examples<a id="examples"></a>
|
|
350
351
|
### RSS Example
|
|
351
352
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/Nashville-Public-Library/talklib/issues)
|
|
5
5
|
[](https://github.com/Nashville-Public-Library/talklib/commits/master)
|
|
6
6
|
|
|
7
|
-
## A package to automate processing
|
|
7
|
+
## A package to automate processing TL shows/segments and podcasts
|
|
8
8
|
|
|
9
9
|
[Skip to Examples](#examples)
|
|
10
10
|
|
|
@@ -24,7 +24,9 @@ Use this module to process the following types of shows/segments:
|
|
|
24
24
|
## Requirements
|
|
25
25
|
|
|
26
26
|
### -[Python](https://www.python.org/downloads/)
|
|
27
|
-
Use Python 3.10 or higher
|
|
27
|
+
Use Python 3.10 or higher.
|
|
28
|
+
|
|
29
|
+
Make sure to select "add to PATH" during installation.
|
|
28
30
|
|
|
29
31
|
### -[FFmpeg](https://www.ffmpeg.org/download.html#build-windows)
|
|
30
32
|
You need Windows binaries for both FFmpeg & FFprobe installed on the PC and added to the PATH:
|
|
@@ -43,11 +45,13 @@ See [below](#disable-twilio) for how to disable Twilio.
|
|
|
43
45
|
### -Environment Variables
|
|
44
46
|
This package uses Environment Variables to help with portability and keep sensitive info separated. The entire list of these is in the `ev.py` file. Make sure to set **all** of these on your PC(s). They are case-sensitive!
|
|
45
47
|
|
|
46
|
-
**ONCE YOU SET/CHANGE/UPDATE ENVIRONMENT VARIABLES ON YOUR PC, YOU NEED TO RESTART WIREREADY**
|
|
48
|
+
> **ONCE YOU SET/CHANGE/UPDATE ENVIRONMENT VARIABLES ON YOUR PC, YOU NEED TO RESTART WIREREADY**
|
|
47
49
|
|
|
48
50
|
---
|
|
49
51
|
## Installation
|
|
50
52
|
|
|
53
|
+
AFTER you have all of the above [requirements](#requirements), inst
|
|
54
|
+
|
|
51
55
|
- Open a terminal/command prompt
|
|
52
56
|
- ````bash
|
|
53
57
|
pip install talklib
|
|
@@ -69,7 +73,10 @@ Before we begin, a general note:
|
|
|
69
73
|
- Instead, we tell WR to run a Batch script (`.bat` file) which in turn will run the Python script (`.py` file).
|
|
70
74
|
- Ensure the Batch & Python scripts are in the same directory.
|
|
71
75
|
- A sample `.bat` file (`Example.bat`) is included in the [misc](https://github.com/Nashville-Public-Library/misc/tree/main/talklib_examples) repo.
|
|
72
|
-
-
|
|
76
|
+
- Download this file and place it in the same folder as your Python file.
|
|
77
|
+
- Right-Click the file and click `Unblock` so that it can be executed.
|
|
78
|
+
- It is a best practice to give the `.bat` and `.py` files the same name, though it is not necessary.
|
|
79
|
+
- PLEASE NOTE: the `.bat` file will run **all** Python files in the folder. This is one reason it is best to separate your Python files into different folders, each with its own `.bat` file.
|
|
73
80
|
|
|
74
81
|
Here is what an example directory structure should look like:
|
|
75
82
|
````
|
|
@@ -78,7 +85,7 @@ D:\wireready
|
|
|
78
85
|
-WP.bat
|
|
79
86
|
-WP.py
|
|
80
87
|
````
|
|
81
|
-
You would
|
|
88
|
+
You would schedule WR to run the `WP.bat` file, which would run the `WP.py` file.
|
|
82
89
|
|
|
83
90
|
----
|
|
84
91
|
|
|
@@ -240,34 +247,6 @@ optional
|
|
|
240
247
|
- be careful with this!
|
|
241
248
|
- default is 21
|
|
242
249
|
|
|
243
|
-
### Notes about formatting:
|
|
244
|
-
|
|
245
|
-
if you're new to Python, here're some reminders
|
|
246
|
-
|
|
247
|
-
### string
|
|
248
|
-
- must be enclosed in quotes (single or double; Python doesn't care)
|
|
249
|
-
|
|
250
|
-
### boolean
|
|
251
|
-
- either True or False
|
|
252
|
-
- do not enclose in quotes
|
|
253
|
-
- must be capitalized
|
|
254
|
-
- correct: True
|
|
255
|
-
- incorrect: true
|
|
256
|
-
|
|
257
|
-
### number
|
|
258
|
-
- in our case, these can be either type int OR float, meaning either whole numbers OR decimal numbers are allowed
|
|
259
|
-
- OK: 5
|
|
260
|
-
- also OK: 5.4
|
|
261
|
-
- do not enclose in quotes
|
|
262
|
-
|
|
263
|
-
## Methods
|
|
264
|
-
|
|
265
|
-
`run()`
|
|
266
|
-
|
|
267
|
-
required
|
|
268
|
-
- executes the script with the attributes you set
|
|
269
|
-
- should be the last line in your script
|
|
270
|
-
|
|
271
250
|
## Examples<a id="examples"></a>
|
|
272
251
|
### RSS Example
|
|
273
252
|
|
|
@@ -1,22 +1,34 @@
|
|
|
1
|
+
aiohappyeyeballs==2.4.0
|
|
1
2
|
aiohttp==3.10.5
|
|
2
3
|
aiohttp-retry==2.8.3
|
|
3
4
|
aiosignal==1.3.1
|
|
5
|
+
annotated-types==0.7.0
|
|
4
6
|
async-timeout==4.0.2
|
|
5
7
|
attrs==22.1.0
|
|
8
|
+
bcrypt==4.2.0
|
|
9
|
+
boto3==1.35.18
|
|
10
|
+
botocore==1.35.18
|
|
6
11
|
build==1.0.3
|
|
7
12
|
certifi==2024.8.30
|
|
13
|
+
cffi==1.17.1
|
|
8
14
|
charset-normalizer==2.1.1
|
|
9
15
|
colorama==0.4.6
|
|
10
16
|
coverage==6.5.0
|
|
17
|
+
cryptography==43.0.3
|
|
18
|
+
decorator==5.1.1
|
|
19
|
+
Deprecated==1.2.14
|
|
11
20
|
docutils==0.20.1
|
|
12
21
|
exceptiongroup==1.0.4
|
|
22
|
+
fabric==3.2.2
|
|
13
23
|
ffmpeg-python==0.2.0
|
|
14
24
|
frozenlist==1.4.1
|
|
15
25
|
future==0.18.3
|
|
16
26
|
idna==3.4
|
|
17
27
|
importlib-metadata==7.0.1
|
|
18
28
|
iniconfig==1.1.1
|
|
29
|
+
invoke==2.2.0
|
|
19
30
|
jaraco.classes==3.3.0
|
|
31
|
+
jmespath==1.0.1
|
|
20
32
|
keyring==24.3.0
|
|
21
33
|
markdown-it-py==3.0.0
|
|
22
34
|
mdurl==0.1.2
|
|
@@ -24,14 +36,20 @@ more-itertools==10.2.0
|
|
|
24
36
|
multidict==6.0.4
|
|
25
37
|
nh3==0.2.15
|
|
26
38
|
packaging==21.3
|
|
39
|
+
paramiko==3.5.0
|
|
27
40
|
pkginfo==1.9.6
|
|
28
41
|
pluggy==1.0.0
|
|
42
|
+
pycparser==2.22
|
|
43
|
+
pydantic==2.9.1
|
|
44
|
+
pydantic_core==2.23.3
|
|
29
45
|
Pygments==2.17.2
|
|
30
46
|
PyJWT==2.6.0
|
|
47
|
+
PyNaCl==1.5.0
|
|
31
48
|
pyparsing==3.0.9
|
|
32
49
|
pyproject_hooks==1.0.0
|
|
33
50
|
pytest==7.2.0
|
|
34
51
|
pytest-cov==4.0.0
|
|
52
|
+
python-dateutil==2.9.0.post0
|
|
35
53
|
pytz==2022.6
|
|
36
54
|
pywin32-ctypes==0.2.2
|
|
37
55
|
readme-renderer==42.0
|
|
@@ -39,9 +57,13 @@ requests==2.32.3
|
|
|
39
57
|
requests-toolbelt==1.0.0
|
|
40
58
|
rfc3986==2.0.0
|
|
41
59
|
rich==13.7.0
|
|
60
|
+
s3transfer==0.10.2
|
|
61
|
+
six==1.16.0
|
|
42
62
|
tomli==2.0.1
|
|
43
63
|
twilio==9.0.2
|
|
44
64
|
twine==4.0.2
|
|
65
|
+
typing_extensions==4.12.2
|
|
45
66
|
urllib3==2.2.2
|
|
67
|
+
wrapt==1.16.0
|
|
46
68
|
yarl==1.9.2
|
|
47
69
|
zipp==3.17.0
|
|
@@ -51,6 +51,7 @@ class EV:
|
|
|
51
51
|
self.twilio_to = sort_twilio_to() # to where should texts/calls be sent
|
|
52
52
|
self.icecast_user = os.environ['icecast_user'] # our icecast username
|
|
53
53
|
self.icecast_pass = os.environ['icecast_pass'] # our icecast password
|
|
54
|
+
self.pod_server_uname = os.environ['pod_server_uname']
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
def sort_destinations():
|
|
@@ -5,6 +5,7 @@ class FFMPEG:
|
|
|
5
5
|
input_file: str = None,
|
|
6
6
|
output_file: str = None,
|
|
7
7
|
breakaway: int|float = 0,
|
|
8
|
+
compression: bool = True,
|
|
8
9
|
compression_level: int|float = 21,
|
|
9
10
|
sample_rate: int = 44100,
|
|
10
11
|
audio_channels: int = 1
|
|
@@ -13,6 +14,7 @@ class FFMPEG:
|
|
|
13
14
|
self.input_file = input_file
|
|
14
15
|
self.output_file = output_file
|
|
15
16
|
self.breakaway = breakaway
|
|
17
|
+
self.compression = compression
|
|
16
18
|
self.compression_level = compression_level
|
|
17
19
|
self.sample_rate = sample_rate
|
|
18
20
|
self.audio_channels = audio_channels
|
|
@@ -29,9 +31,12 @@ class FFMPEG:
|
|
|
29
31
|
command = {}
|
|
30
32
|
command.update({'ar': self.sample_rate})
|
|
31
33
|
command.update({'ac': self.audio_channels})
|
|
32
|
-
|
|
34
|
+
if self.compression:
|
|
35
|
+
command.update({'af': f'loudnorm=I=-{self.compression_level}'})
|
|
33
36
|
if self.breakaway:
|
|
34
37
|
command.update({'t': self.breakaway})
|
|
38
|
+
if self.output_file.endswith('mp3'):
|
|
39
|
+
command.update({'b:a': "96k"})
|
|
35
40
|
command.update({'y': None})
|
|
36
41
|
command.update({'filename': self.output_file})
|
|
37
42
|
|
|
@@ -94,7 +94,11 @@ class Notify:
|
|
|
94
94
|
format['Subject'] = subject
|
|
95
95
|
format['From'] = self.EV.fromEmail
|
|
96
96
|
format['To'] = email
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
try:
|
|
98
|
+
mail = smtplib.SMTP(host=self.EV.mail_server)
|
|
99
|
+
mail.send_message(format)
|
|
100
|
+
mail.quit()
|
|
101
|
+
except ConnectionRefusedError as e:
|
|
102
|
+
print(e)
|
|
103
|
+
except TimeoutError as e:
|
|
104
|
+
print(e)
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
import glob
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from typing import ClassVar, Type
|
|
9
|
+
|
|
10
|
+
from fabric import Connection, Result
|
|
11
|
+
from pydantic import BaseModel, Field, model_validator
|
|
12
|
+
|
|
13
|
+
from talklib.ev import EV
|
|
14
|
+
from talklib.notify import Notify
|
|
15
|
+
from talklib.ffmpeg import FFMPEG
|
|
16
|
+
from talklib.utils import today_is_weekday
|
|
17
|
+
|
|
18
|
+
class Notifications(BaseModel):
|
|
19
|
+
prefix: ClassVar[str] = None # prefix all messages with identifier for the show/podcast
|
|
20
|
+
notify: Type[Notify] = Notify()
|
|
21
|
+
|
|
22
|
+
def prep_syslog(self, message: str, level: str = 'info'):
|
|
23
|
+
'''send message to syslog server'''
|
|
24
|
+
message = f'{self.prefix}: {message}'
|
|
25
|
+
print(message)
|
|
26
|
+
self.notify.send_syslog(message=message, level=level)
|
|
27
|
+
|
|
28
|
+
def prep_send_mail(self, message: str, subject: str):
|
|
29
|
+
'''send email to TL gmail account via relay address'''
|
|
30
|
+
subject = f'{subject}: {self.prefix}'
|
|
31
|
+
self.notify.send_mail(subject=subject, message=message)
|
|
32
|
+
|
|
33
|
+
def send_sms_if_enabled(self, message: str):
|
|
34
|
+
'''send sms via twilio IF twilio_enable is set to True'''
|
|
35
|
+
self.notify.send_sms(message=message)
|
|
36
|
+
|
|
37
|
+
def send_notifications(self, message: str, subject: str, syslog_level: str = 'error'):
|
|
38
|
+
'''we generally only want to send SMS via Twilio if today is on a weekend'''
|
|
39
|
+
if today_is_weekday():
|
|
40
|
+
self.prep_send_mail(message=message, subject=subject)
|
|
41
|
+
self.prep_syslog(message=message, level=syslog_level)
|
|
42
|
+
else:
|
|
43
|
+
self.send_sms_if_enabled(message=message)
|
|
44
|
+
self.prep_send_mail(message=message, subject=subject)
|
|
45
|
+
self.prep_syslog(message=message, level=syslog_level)
|
|
46
|
+
|
|
47
|
+
class SSH(BaseModel):
|
|
48
|
+
server: str = "assets.library.nashville.org"
|
|
49
|
+
user: str = EV().pod_server_uname
|
|
50
|
+
connection: Type[Connection] = Connection(host=server, user=user)
|
|
51
|
+
notifications: Notifications = Notifications()
|
|
52
|
+
|
|
53
|
+
def upload_file(self, file: str, folder: str) -> None:
|
|
54
|
+
self.check_folder_exists(folder=folder)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
self.notifications.prep_syslog(message=f"Attempting to upload '{file}' to {folder}/")
|
|
58
|
+
self.connection.put(local=file, remote=f"talkinglibrary/shows/{folder}", preserve_mode=False)
|
|
59
|
+
self.notifications.prep_syslog(message=f"Successfully uploaded '{file}' to {folder}/")
|
|
60
|
+
return
|
|
61
|
+
except (FileNotFoundError, Exception) as e:
|
|
62
|
+
to_send = f"unable to upload '{file}': {e}"
|
|
63
|
+
self.notifications.send_notifications(message=to_send, subject='Error')
|
|
64
|
+
raise e
|
|
65
|
+
|
|
66
|
+
def download_file(self, file: str, folder: str) -> None:
|
|
67
|
+
try:
|
|
68
|
+
self.notifications.prep_syslog(message=f"Attempting to download '{file}' from {folder}/")
|
|
69
|
+
self.connection.get(remote=f"talkinglibrary/shows/{folder}/{file}")
|
|
70
|
+
self.notifications.prep_syslog(message=f"Successfully downloaded '{file}' to {os.getcwd()}")
|
|
71
|
+
return file
|
|
72
|
+
except Exception as e:
|
|
73
|
+
to_send = f"unable to download '{file}': {e}"
|
|
74
|
+
self.notifications.send_notifications(message=to_send, subject='Error')
|
|
75
|
+
raise e
|
|
76
|
+
|
|
77
|
+
def delete_file(self, file: str, folder: str) -> None:
|
|
78
|
+
try:
|
|
79
|
+
self.notifications.prep_syslog(message=f"Attempting to delete '{file}' from {folder}/")
|
|
80
|
+
self.connection.sftp().remove(f"talkinglibrary/shows/{folder}/{file}")
|
|
81
|
+
self.notifications.prep_syslog(message=f"Successfully deleted '{file}' from {folder}/")
|
|
82
|
+
return
|
|
83
|
+
except (Exception, FileNotFoundError) as e:
|
|
84
|
+
self.notifications.send_notifications(
|
|
85
|
+
message=f"Unable to delete '{file}' from {folder}: {e}. Continuing automation...",
|
|
86
|
+
subject="Error")
|
|
87
|
+
|
|
88
|
+
def get_folders(self) -> list:
|
|
89
|
+
ret_val: list = []
|
|
90
|
+
result: Result = self.connection.run("cd talkinglibrary/shows && ls", hide=True)
|
|
91
|
+
result_stdout:str = result.stdout
|
|
92
|
+
folders: list[str] = result_stdout.rsplit("\n")
|
|
93
|
+
for folder in folders:
|
|
94
|
+
if folder != "":
|
|
95
|
+
ret_val.append(folder.lower())
|
|
96
|
+
return ret_val
|
|
97
|
+
|
|
98
|
+
def get_files_from_folder(self, folder: str) -> list:
|
|
99
|
+
ret_val: list = []
|
|
100
|
+
result: Result = self.connection.run(f"cd talkinglibrary/shows/{folder} && ls", hide=True)
|
|
101
|
+
result_stdout:str = result.stdout
|
|
102
|
+
files: list[str] = result_stdout.rsplit("\n")
|
|
103
|
+
for file in files:
|
|
104
|
+
if file != "": # exclude files without names?
|
|
105
|
+
ret_val.append(file.lower())
|
|
106
|
+
return ret_val
|
|
107
|
+
|
|
108
|
+
def get_all_files(self) -> list:
|
|
109
|
+
ret_val: list = []
|
|
110
|
+
result: Result = self.connection.run("cd talkinglibrary/shows && ls -R", hide=True)
|
|
111
|
+
result_stdout:str = result.stdout
|
|
112
|
+
files: list[str] = result_stdout.rsplit("\n")
|
|
113
|
+
for file in files:
|
|
114
|
+
if not file.startswith("./") and file != "": # exclude folders and files without names
|
|
115
|
+
ret_val.append(file.lower())
|
|
116
|
+
return ret_val
|
|
117
|
+
|
|
118
|
+
def check_folder_exists(self, folder: str) -> bool:
|
|
119
|
+
self.notifications.prep_syslog(message=f"checking if {folder}/ exists on server...")
|
|
120
|
+
folders = self.get_folders()
|
|
121
|
+
if folder.lower() in folders:
|
|
122
|
+
self.notifications.prep_syslog(message=f"{folder}/ exists!")
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
to_send = f"cannot find folder titled: {folder}/ on server"
|
|
126
|
+
self.notifications.send_notifications(message=to_send, subject='Error')
|
|
127
|
+
raise Exception (to_send)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Episode(BaseModel):
|
|
131
|
+
feed_file: str = Field(min_length=1, default=None)
|
|
132
|
+
audio_filename: str = Field(min_length=1, default=None)
|
|
133
|
+
bucket_folder: str = Field(min_length=1, default=None)
|
|
134
|
+
episode_title: str = Field(min_length=1, default=None)
|
|
135
|
+
notifications: Notifications = Notifications()
|
|
136
|
+
max_episodes: int = Field(default=None)
|
|
137
|
+
categories: list = Field(default=None)
|
|
138
|
+
|
|
139
|
+
def pub_date(self) -> str:
|
|
140
|
+
timezone = time.timezone/60/60 # 60 seconds per minute, 60 minutes per hour
|
|
141
|
+
timezone = round(timezone)
|
|
142
|
+
if time.localtime().tm_isdst: # if DST is currently active
|
|
143
|
+
timezone-=1
|
|
144
|
+
|
|
145
|
+
pub_date = datetime.now().strftime(f'%a, %d %b %Y %H:%M:%S -0{timezone}00')
|
|
146
|
+
self.notifications.prep_syslog(message=f"pubDate will be: {pub_date}")
|
|
147
|
+
return pub_date
|
|
148
|
+
|
|
149
|
+
def size_in_bytes(self, filename) -> str:
|
|
150
|
+
size_in_bytes = os.path.getsize(filename)
|
|
151
|
+
size_in_bytes = str(size_in_bytes)
|
|
152
|
+
self.notifications.prep_syslog(message=f"enclosure lenth will be {size_in_bytes}")
|
|
153
|
+
return size_in_bytes
|
|
154
|
+
|
|
155
|
+
def enclosure(self) -> str:
|
|
156
|
+
enclosure = f"https://assets.library.nashville.org/talkinglibrary/shows/{self.bucket_folder}/{self.audio_filename}"
|
|
157
|
+
self.notifications.prep_syslog(message=f"enclosure will be {enclosure}")
|
|
158
|
+
return enclosure
|
|
159
|
+
|
|
160
|
+
def itunes_duration(self) -> str:
|
|
161
|
+
ffmpeg = FFMPEG(input_file=self.audio_filename)
|
|
162
|
+
duration = ffmpeg.get_length_in_minutes()
|
|
163
|
+
seconds, minutes = math.modf(duration)
|
|
164
|
+
|
|
165
|
+
minutes = round(minutes)
|
|
166
|
+
|
|
167
|
+
seconds = seconds*60
|
|
168
|
+
seconds = round(seconds)
|
|
169
|
+
|
|
170
|
+
result = f"{minutes}:{seconds:02}"
|
|
171
|
+
self.notifications.prep_syslog(message=f"itunes:duration will be {result}")
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
def add_new_episode(self):
|
|
175
|
+
'''Create an 'item' element. Then create all of the necessary sub elements and append them to the item element'''
|
|
176
|
+
ET.register_namespace(prefix="atom", uri="http://www.w3.org/2005/Atom")
|
|
177
|
+
ET.register_namespace(prefix="itunes", uri="http://www.itunes.com/dtds/podcast-1.0.dtd")
|
|
178
|
+
feed = ET.parse(self.feed_file)
|
|
179
|
+
root = feed.getroot()
|
|
180
|
+
root = root.find('channel')
|
|
181
|
+
|
|
182
|
+
item = ET.Element('item')
|
|
183
|
+
|
|
184
|
+
title = ET.Element('title')
|
|
185
|
+
title.text = self.episode_title
|
|
186
|
+
item.append(title)
|
|
187
|
+
|
|
188
|
+
pubDate = ET.Element('pubDate')
|
|
189
|
+
pubDate.text = self.pub_date()
|
|
190
|
+
item.append(pubDate)
|
|
191
|
+
|
|
192
|
+
enclosure = ET.Element('enclosure')
|
|
193
|
+
enclosure.set('url', self.enclosure())
|
|
194
|
+
enclosure.set('length', self.size_in_bytes(self.audio_filename))
|
|
195
|
+
enclosure.set('type', 'audio/mpeg')
|
|
196
|
+
item.append(enclosure)
|
|
197
|
+
|
|
198
|
+
guid = ET.Element('guid')
|
|
199
|
+
guid.set('isPermaLink', 'false')
|
|
200
|
+
guid.text = self.audio_filename # this is just the name of the audio file. useful for deleting the file later on...
|
|
201
|
+
item.append(guid)
|
|
202
|
+
|
|
203
|
+
itunes_duration_element = ET.Element("itunes:duration")
|
|
204
|
+
itunes_duration_element.text = self.itunes_duration()
|
|
205
|
+
item.append(itunes_duration_element)
|
|
206
|
+
|
|
207
|
+
for category in self.categories:
|
|
208
|
+
category_element = ET.Element("category")
|
|
209
|
+
category_element.text = category
|
|
210
|
+
self.notifications.prep_syslog(message=f"adding category: {category}")
|
|
211
|
+
item.append(category_element)
|
|
212
|
+
|
|
213
|
+
# insert the new 'item' element as the first item, but below all the other channel elements
|
|
214
|
+
items = root.findall('item')
|
|
215
|
+
if items:
|
|
216
|
+
# If there are existing items (there usually will be), add the new item before to the top
|
|
217
|
+
first_item = items[0]
|
|
218
|
+
index = list(root).index(first_item)
|
|
219
|
+
self.notifications.prep_syslog(message=f"adding {item} to feed at {index}")
|
|
220
|
+
root.insert(index, item)
|
|
221
|
+
else:
|
|
222
|
+
# If no items exist, add the new item to the bottom (after the other channel elements)
|
|
223
|
+
root.append(item)
|
|
224
|
+
|
|
225
|
+
last_build_date = root.find('lastBuildDate')
|
|
226
|
+
last_build_date.text = self.pub_date() # fine to use this same pub date, as the format for both is the same
|
|
227
|
+
|
|
228
|
+
ET.indent(feed) # makes the XML pretty looking
|
|
229
|
+
self.notifications.prep_syslog(message=f"writing feed file...")
|
|
230
|
+
feed.write(self.feed_file, encoding="utf-8", xml_declaration=True)
|
|
231
|
+
|
|
232
|
+
def remove_old_episodes(self):
|
|
233
|
+
ET.register_namespace(prefix="atom", uri="http://www.w3.org/2005/Atom")
|
|
234
|
+
ET.register_namespace(prefix="itunes", uri="http://www.itunes.com/dtds/podcast-1.0.dtd")
|
|
235
|
+
feed = ET.parse(self.feed_file)
|
|
236
|
+
root = feed.getroot()
|
|
237
|
+
root = root.find('channel')
|
|
238
|
+
items = root.findall('item')
|
|
239
|
+
number_of_items = len(items)
|
|
240
|
+
index = -1
|
|
241
|
+
while number_of_items > self.max_episodes:
|
|
242
|
+
self.notifications.prep_syslog(message=f"There are currently {number_of_items} items in the feed, which is above {self.max_episodes}")
|
|
243
|
+
item_to_remove = items[index] # locate last item in feed
|
|
244
|
+
guid = item_to_remove.find('guid').text # grab the guid (filename) so we can delete the file from the server
|
|
245
|
+
self.notifications.prep_syslog(f'removing from feed: {item_to_remove}')
|
|
246
|
+
root.remove(item_to_remove)
|
|
247
|
+
|
|
248
|
+
SSH().delete_file(folder=self.bucket_folder, file=guid)
|
|
249
|
+
|
|
250
|
+
ET.indent(feed)
|
|
251
|
+
self.notifications.prep_syslog(message=f"writing to feed file...")
|
|
252
|
+
feed.write(self.feed_file, encoding="utf-8", xml_declaration=True)
|
|
253
|
+
number_of_items-=1
|
|
254
|
+
index-=1
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TLPod(BaseModel):
|
|
258
|
+
'''
|
|
259
|
+
everything should be in lower case!
|
|
260
|
+
|
|
261
|
+
display_name: generic name for the show/program. WIll be displayed as the
|
|
262
|
+
episode "Title" in the podcast feed. type=string
|
|
263
|
+
|
|
264
|
+
filename_to_match: the base name of the show we want to match. do not include the date.
|
|
265
|
+
for example, to match RollingStone091322, use 'RollingStone'. type=string
|
|
266
|
+
|
|
267
|
+
bucket_folder: the name of the folder on S3 where the audio and RSS files are stored.
|
|
268
|
+
should be lower case. type=string
|
|
269
|
+
|
|
270
|
+
max_episodes_in_feed: the max number of episodes that should be in the feed after you add the episode.
|
|
271
|
+
'''
|
|
272
|
+
display_name: str = Field(min_length=1)
|
|
273
|
+
filename_to_match: str = Field(min_length=1)
|
|
274
|
+
categories: list = Field(default=[])
|
|
275
|
+
bucket_folder: str = Field(default=None)
|
|
276
|
+
max_episodes_in_feed: int = Field(ge=1, default=5)
|
|
277
|
+
override_filename: bool = False
|
|
278
|
+
audio_folders:list = EV().destinations
|
|
279
|
+
notifications: Type[Notifications] = Notifications()
|
|
280
|
+
episode: Type[Episode] = Episode(notifications=notifications)
|
|
281
|
+
ffmpeg: Type[FFMPEG] = FFMPEG()
|
|
282
|
+
ssh: Type[SSH] = SSH(notifications=notifications)
|
|
283
|
+
|
|
284
|
+
@model_validator(mode='after')
|
|
285
|
+
def post_update(self):
|
|
286
|
+
'''
|
|
287
|
+
the name of the bucket folder should match the base name of the file. If bucket_folder is not explicitly set
|
|
288
|
+
by the user, use the filename. However, if filename_override is being used, strip out the digits first.
|
|
289
|
+
'''
|
|
290
|
+
if not self.bucket_folder:
|
|
291
|
+
if self.override_filename:
|
|
292
|
+
self.bucket_folder = re.sub(pattern="[0-9]", string=self.filename_to_match.lower(), repl='')
|
|
293
|
+
else:
|
|
294
|
+
self.bucket_folder = self.filename_to_match.lower()
|
|
295
|
+
|
|
296
|
+
prefix = f"{self.display_name} (podcast)"
|
|
297
|
+
Notifications.prefix = prefix
|
|
298
|
+
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def get_filename_to_match(self) -> str:
|
|
302
|
+
if self.override_filename:
|
|
303
|
+
self.notifications.prep_syslog(message="filename override is turned ON")
|
|
304
|
+
return self.filename_to_match.lower()
|
|
305
|
+
today_date: str = datetime.now().strftime("%m%d%y") # this is how we date our programs: MMDDYY
|
|
306
|
+
return (self.filename_to_match + today_date).lower()
|
|
307
|
+
|
|
308
|
+
def match_file(self):
|
|
309
|
+
'''match the name of the program that has today's date in the filename'''
|
|
310
|
+
to_match = self.get_filename_to_match()
|
|
311
|
+
self.notifications.prep_syslog(message=f"looking for file to match: {to_match}")
|
|
312
|
+
for dest in self.audio_folders:
|
|
313
|
+
self.notifications.prep_syslog(message=f"searching for {to_match} in {dest}...")
|
|
314
|
+
files = glob.glob(f"{dest}/*.wav")
|
|
315
|
+
for file in files:
|
|
316
|
+
if to_match in file.lower():
|
|
317
|
+
self.notifications.prep_syslog(message="found matching file!")
|
|
318
|
+
return file
|
|
319
|
+
to_send = f"There was a problem podcasting {self.display_name}. Cannot find matched file {to_match} in {self.audio_folders}"
|
|
320
|
+
self.notifications.send_notifications(message=to_send, subject='Error')
|
|
321
|
+
raise FileNotFoundError
|
|
322
|
+
|
|
323
|
+
def convert(self, file:str):
|
|
324
|
+
output_filename = file.split('.')
|
|
325
|
+
output_filename = output_filename[0]
|
|
326
|
+
output_filename = os.path.basename(output_filename).lower()
|
|
327
|
+
output_filename = output_filename + '.mp3'
|
|
328
|
+
self.notifications.prep_syslog(message=f"Converted audio file will be {output_filename}")
|
|
329
|
+
|
|
330
|
+
self.ffmpeg.input_file = file
|
|
331
|
+
self.ffmpeg.output_file = output_filename
|
|
332
|
+
self.ffmpeg.compression = False # this is for podcasts. these files should already be edited
|
|
333
|
+
|
|
334
|
+
ffmpeg_commands = self.ffmpeg.get_commands()
|
|
335
|
+
self.notifications.prep_syslog(message=f"FFmppeg commands: {ffmpeg_commands}")
|
|
336
|
+
|
|
337
|
+
self.notifications.prep_syslog(message=f"Converting {file} to mp3...")
|
|
338
|
+
try:
|
|
339
|
+
output = self.ffmpeg.convert()
|
|
340
|
+
self.notifications.prep_syslog(message="File successfully converted")
|
|
341
|
+
return output
|
|
342
|
+
except Exception as ffmpeg_exception:
|
|
343
|
+
self.notifications.send_notifications(message=f'FFmpeg error: {ffmpeg_exception}', subject='Error')
|
|
344
|
+
raise ffmpeg_exception
|
|
345
|
+
|
|
346
|
+
def delete_local_file(self, file: str):
|
|
347
|
+
try:
|
|
348
|
+
self.notifications.prep_syslog(message=f"Attempting to delete local file: {file}")
|
|
349
|
+
os.remove(file)
|
|
350
|
+
self.notifications.prep_syslog(message=f"Successfully deleted local file: {file}")
|
|
351
|
+
return
|
|
352
|
+
except Exception:
|
|
353
|
+
self.notifications.prep_syslog(message=f"Unable to delete local file: {file}")
|
|
354
|
+
|
|
355
|
+
def run(self):
|
|
356
|
+
self.notifications.prep_syslog(message="Starting up...")
|
|
357
|
+
|
|
358
|
+
self.ssh.check_folder_exists(folder=self.bucket_folder)
|
|
359
|
+
|
|
360
|
+
audio_file = self.match_file()
|
|
361
|
+
converted_file = self.convert(file=audio_file)
|
|
362
|
+
|
|
363
|
+
feed_file = self.ssh.download_file(folder=self.bucket_folder, file='feed.xml') # all XML files on server should have the same name
|
|
364
|
+
|
|
365
|
+
self.episode.feed_file = feed_file
|
|
366
|
+
self.episode.audio_filename = converted_file
|
|
367
|
+
self.episode.bucket_folder = self.bucket_folder
|
|
368
|
+
self.episode.categories = self.categories
|
|
369
|
+
self.episode.episode_title = f"{self.display_name} ({datetime.now().strftime('%a, %d %B')})"
|
|
370
|
+
self.episode.max_episodes = self.max_episodes_in_feed
|
|
371
|
+
|
|
372
|
+
self.episode.add_new_episode()
|
|
373
|
+
self.episode.remove_old_episodes()
|
|
374
|
+
|
|
375
|
+
self.ssh.upload_file(folder=self.bucket_folder, file=converted_file)
|
|
376
|
+
self.ssh.upload_file(folder=self.bucket_folder, file=feed_file)
|
|
377
|
+
|
|
378
|
+
self.delete_local_file(file=feed_file)
|
|
379
|
+
self.delete_local_file(file=converted_file)
|
|
380
|
+
|
|
381
|
+
self.notifications.prep_syslog(message="All done.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: talklib
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: A package to automate processing of shows/segments airing on the TL
|
|
5
5
|
Author-email: Ben Weddle <ben.weddle@gmail.com>
|
|
6
6
|
Maintainer-email: Ben Weddle <ben.weddle@gmail.com>
|
|
@@ -28,25 +28,37 @@ Project-URL: Issues, https://github.com/talkinglibrary/talklib/issues
|
|
|
28
28
|
Requires-Python: >=3.10
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
License-File: LICENSE.txt
|
|
31
|
+
Requires-Dist: aiohappyeyeballs==2.4.0
|
|
31
32
|
Requires-Dist: aiohttp==3.10.5
|
|
32
33
|
Requires-Dist: aiohttp-retry==2.8.3
|
|
33
34
|
Requires-Dist: aiosignal==1.3.1
|
|
35
|
+
Requires-Dist: annotated-types==0.7.0
|
|
34
36
|
Requires-Dist: async-timeout==4.0.2
|
|
35
37
|
Requires-Dist: attrs==22.1.0
|
|
38
|
+
Requires-Dist: bcrypt==4.2.0
|
|
39
|
+
Requires-Dist: boto3==1.35.18
|
|
40
|
+
Requires-Dist: botocore==1.35.18
|
|
36
41
|
Requires-Dist: build==1.0.3
|
|
37
42
|
Requires-Dist: certifi==2024.8.30
|
|
43
|
+
Requires-Dist: cffi==1.17.1
|
|
38
44
|
Requires-Dist: charset-normalizer==2.1.1
|
|
39
45
|
Requires-Dist: colorama==0.4.6
|
|
40
46
|
Requires-Dist: coverage==6.5.0
|
|
47
|
+
Requires-Dist: cryptography==43.0.3
|
|
48
|
+
Requires-Dist: decorator==5.1.1
|
|
49
|
+
Requires-Dist: Deprecated==1.2.14
|
|
41
50
|
Requires-Dist: docutils==0.20.1
|
|
42
51
|
Requires-Dist: exceptiongroup==1.0.4
|
|
52
|
+
Requires-Dist: fabric==3.2.2
|
|
43
53
|
Requires-Dist: ffmpeg-python==0.2.0
|
|
44
54
|
Requires-Dist: frozenlist==1.4.1
|
|
45
55
|
Requires-Dist: future==0.18.3
|
|
46
56
|
Requires-Dist: idna==3.4
|
|
47
57
|
Requires-Dist: importlib-metadata==7.0.1
|
|
48
58
|
Requires-Dist: iniconfig==1.1.1
|
|
59
|
+
Requires-Dist: invoke==2.2.0
|
|
49
60
|
Requires-Dist: jaraco.classes==3.3.0
|
|
61
|
+
Requires-Dist: jmespath==1.0.1
|
|
50
62
|
Requires-Dist: keyring==24.3.0
|
|
51
63
|
Requires-Dist: markdown-it-py==3.0.0
|
|
52
64
|
Requires-Dist: mdurl==0.1.2
|
|
@@ -54,14 +66,20 @@ Requires-Dist: more-itertools==10.2.0
|
|
|
54
66
|
Requires-Dist: multidict==6.0.4
|
|
55
67
|
Requires-Dist: nh3==0.2.15
|
|
56
68
|
Requires-Dist: packaging==21.3
|
|
69
|
+
Requires-Dist: paramiko==3.5.0
|
|
57
70
|
Requires-Dist: pkginfo==1.9.6
|
|
58
71
|
Requires-Dist: pluggy==1.0.0
|
|
72
|
+
Requires-Dist: pycparser==2.22
|
|
73
|
+
Requires-Dist: pydantic==2.9.1
|
|
74
|
+
Requires-Dist: pydantic_core==2.23.3
|
|
59
75
|
Requires-Dist: Pygments==2.17.2
|
|
60
76
|
Requires-Dist: PyJWT==2.6.0
|
|
77
|
+
Requires-Dist: PyNaCl==1.5.0
|
|
61
78
|
Requires-Dist: pyparsing==3.0.9
|
|
62
79
|
Requires-Dist: pyproject_hooks==1.0.0
|
|
63
80
|
Requires-Dist: pytest==7.2.0
|
|
64
81
|
Requires-Dist: pytest-cov==4.0.0
|
|
82
|
+
Requires-Dist: python-dateutil==2.9.0.post0
|
|
65
83
|
Requires-Dist: pytz==2022.6
|
|
66
84
|
Requires-Dist: pywin32-ctypes==0.2.2
|
|
67
85
|
Requires-Dist: readme-renderer==42.0
|
|
@@ -69,10 +87,14 @@ Requires-Dist: requests==2.32.3
|
|
|
69
87
|
Requires-Dist: requests-toolbelt==1.0.0
|
|
70
88
|
Requires-Dist: rfc3986==2.0.0
|
|
71
89
|
Requires-Dist: rich==13.7.0
|
|
90
|
+
Requires-Dist: s3transfer==0.10.2
|
|
91
|
+
Requires-Dist: six==1.16.0
|
|
72
92
|
Requires-Dist: tomli==2.0.1
|
|
73
93
|
Requires-Dist: twilio==9.0.2
|
|
74
94
|
Requires-Dist: twine==4.0.2
|
|
95
|
+
Requires-Dist: typing_extensions==4.12.2
|
|
75
96
|
Requires-Dist: urllib3==2.2.2
|
|
97
|
+
Requires-Dist: wrapt==1.16.0
|
|
76
98
|
Requires-Dist: yarl==1.9.2
|
|
77
99
|
Requires-Dist: zipp==3.17.0
|
|
78
100
|
|
|
@@ -82,7 +104,7 @@ Requires-Dist: zipp==3.17.0
|
|
|
82
104
|
[](https://github.com/Nashville-Public-Library/talklib/issues)
|
|
83
105
|
[](https://github.com/Nashville-Public-Library/talklib/commits/master)
|
|
84
106
|
|
|
85
|
-
## A package to automate processing
|
|
107
|
+
## A package to automate processing TL shows/segments and podcasts
|
|
86
108
|
|
|
87
109
|
[Skip to Examples](#examples)
|
|
88
110
|
|
|
@@ -102,7 +124,9 @@ Use this module to process the following types of shows/segments:
|
|
|
102
124
|
## Requirements
|
|
103
125
|
|
|
104
126
|
### -[Python](https://www.python.org/downloads/)
|
|
105
|
-
Use Python 3.10 or higher
|
|
127
|
+
Use Python 3.10 or higher.
|
|
128
|
+
|
|
129
|
+
Make sure to select "add to PATH" during installation.
|
|
106
130
|
|
|
107
131
|
### -[FFmpeg](https://www.ffmpeg.org/download.html#build-windows)
|
|
108
132
|
You need Windows binaries for both FFmpeg & FFprobe installed on the PC and added to the PATH:
|
|
@@ -121,11 +145,13 @@ See [below](#disable-twilio) for how to disable Twilio.
|
|
|
121
145
|
### -Environment Variables
|
|
122
146
|
This package uses Environment Variables to help with portability and keep sensitive info separated. The entire list of these is in the `ev.py` file. Make sure to set **all** of these on your PC(s). They are case-sensitive!
|
|
123
147
|
|
|
124
|
-
**ONCE YOU SET/CHANGE/UPDATE ENVIRONMENT VARIABLES ON YOUR PC, YOU NEED TO RESTART WIREREADY**
|
|
148
|
+
> **ONCE YOU SET/CHANGE/UPDATE ENVIRONMENT VARIABLES ON YOUR PC, YOU NEED TO RESTART WIREREADY**
|
|
125
149
|
|
|
126
150
|
---
|
|
127
151
|
## Installation
|
|
128
152
|
|
|
153
|
+
AFTER you have all of the above [requirements](#requirements), inst
|
|
154
|
+
|
|
129
155
|
- Open a terminal/command prompt
|
|
130
156
|
- ````bash
|
|
131
157
|
pip install talklib
|
|
@@ -147,7 +173,10 @@ Before we begin, a general note:
|
|
|
147
173
|
- Instead, we tell WR to run a Batch script (`.bat` file) which in turn will run the Python script (`.py` file).
|
|
148
174
|
- Ensure the Batch & Python scripts are in the same directory.
|
|
149
175
|
- A sample `.bat` file (`Example.bat`) is included in the [misc](https://github.com/Nashville-Public-Library/misc/tree/main/talklib_examples) repo.
|
|
150
|
-
-
|
|
176
|
+
- Download this file and place it in the same folder as your Python file.
|
|
177
|
+
- Right-Click the file and click `Unblock` so that it can be executed.
|
|
178
|
+
- It is a best practice to give the `.bat` and `.py` files the same name, though it is not necessary.
|
|
179
|
+
- PLEASE NOTE: the `.bat` file will run **all** Python files in the folder. This is one reason it is best to separate your Python files into different folders, each with its own `.bat` file.
|
|
151
180
|
|
|
152
181
|
Here is what an example directory structure should look like:
|
|
153
182
|
````
|
|
@@ -156,7 +185,7 @@ D:\wireready
|
|
|
156
185
|
-WP.bat
|
|
157
186
|
-WP.py
|
|
158
187
|
````
|
|
159
|
-
You would
|
|
188
|
+
You would schedule WR to run the `WP.bat` file, which would run the `WP.py` file.
|
|
160
189
|
|
|
161
190
|
----
|
|
162
191
|
|
|
@@ -318,34 +347,6 @@ optional
|
|
|
318
347
|
- be careful with this!
|
|
319
348
|
- default is 21
|
|
320
349
|
|
|
321
|
-
### Notes about formatting:
|
|
322
|
-
|
|
323
|
-
if you're new to Python, here're some reminders
|
|
324
|
-
|
|
325
|
-
### string
|
|
326
|
-
- must be enclosed in quotes (single or double; Python doesn't care)
|
|
327
|
-
|
|
328
|
-
### boolean
|
|
329
|
-
- either True or False
|
|
330
|
-
- do not enclose in quotes
|
|
331
|
-
- must be capitalized
|
|
332
|
-
- correct: True
|
|
333
|
-
- incorrect: true
|
|
334
|
-
|
|
335
|
-
### number
|
|
336
|
-
- in our case, these can be either type int OR float, meaning either whole numbers OR decimal numbers are allowed
|
|
337
|
-
- OK: 5
|
|
338
|
-
- also OK: 5.4
|
|
339
|
-
- do not enclose in quotes
|
|
340
|
-
|
|
341
|
-
## Methods
|
|
342
|
-
|
|
343
|
-
`run()`
|
|
344
|
-
|
|
345
|
-
required
|
|
346
|
-
- executes the script with the attributes you set
|
|
347
|
-
- should be the last line in your script
|
|
348
|
-
|
|
349
350
|
## Examples<a id="examples"></a>
|
|
350
351
|
### RSS Example
|
|
351
352
|
|
|
@@ -1,22 +1,34 @@
|
|
|
1
|
+
aiohappyeyeballs==2.4.0
|
|
1
2
|
aiohttp==3.10.5
|
|
2
3
|
aiohttp-retry==2.8.3
|
|
3
4
|
aiosignal==1.3.1
|
|
5
|
+
annotated-types==0.7.0
|
|
4
6
|
async-timeout==4.0.2
|
|
5
7
|
attrs==22.1.0
|
|
8
|
+
bcrypt==4.2.0
|
|
9
|
+
boto3==1.35.18
|
|
10
|
+
botocore==1.35.18
|
|
6
11
|
build==1.0.3
|
|
7
12
|
certifi==2024.8.30
|
|
13
|
+
cffi==1.17.1
|
|
8
14
|
charset-normalizer==2.1.1
|
|
9
15
|
colorama==0.4.6
|
|
10
16
|
coverage==6.5.0
|
|
17
|
+
cryptography==43.0.3
|
|
18
|
+
decorator==5.1.1
|
|
19
|
+
Deprecated==1.2.14
|
|
11
20
|
docutils==0.20.1
|
|
12
21
|
exceptiongroup==1.0.4
|
|
22
|
+
fabric==3.2.2
|
|
13
23
|
ffmpeg-python==0.2.0
|
|
14
24
|
frozenlist==1.4.1
|
|
15
25
|
future==0.18.3
|
|
16
26
|
idna==3.4
|
|
17
27
|
importlib-metadata==7.0.1
|
|
18
28
|
iniconfig==1.1.1
|
|
29
|
+
invoke==2.2.0
|
|
19
30
|
jaraco.classes==3.3.0
|
|
31
|
+
jmespath==1.0.1
|
|
20
32
|
keyring==24.3.0
|
|
21
33
|
markdown-it-py==3.0.0
|
|
22
34
|
mdurl==0.1.2
|
|
@@ -24,14 +36,20 @@ more-itertools==10.2.0
|
|
|
24
36
|
multidict==6.0.4
|
|
25
37
|
nh3==0.2.15
|
|
26
38
|
packaging==21.3
|
|
39
|
+
paramiko==3.5.0
|
|
27
40
|
pkginfo==1.9.6
|
|
28
41
|
pluggy==1.0.0
|
|
42
|
+
pycparser==2.22
|
|
43
|
+
pydantic==2.9.1
|
|
44
|
+
pydantic_core==2.23.3
|
|
29
45
|
Pygments==2.17.2
|
|
30
46
|
PyJWT==2.6.0
|
|
47
|
+
PyNaCl==1.5.0
|
|
31
48
|
pyparsing==3.0.9
|
|
32
49
|
pyproject_hooks==1.0.0
|
|
33
50
|
pytest==7.2.0
|
|
34
51
|
pytest-cov==4.0.0
|
|
52
|
+
python-dateutil==2.9.0.post0
|
|
35
53
|
pytz==2022.6
|
|
36
54
|
pywin32-ctypes==0.2.2
|
|
37
55
|
readme-renderer==42.0
|
|
@@ -39,9 +57,13 @@ requests==2.32.3
|
|
|
39
57
|
requests-toolbelt==1.0.0
|
|
40
58
|
rfc3986==2.0.0
|
|
41
59
|
rich==13.7.0
|
|
60
|
+
s3transfer==0.10.2
|
|
61
|
+
six==1.16.0
|
|
42
62
|
tomli==2.0.1
|
|
43
63
|
twilio==9.0.2
|
|
44
64
|
twine==4.0.2
|
|
65
|
+
typing_extensions==4.12.2
|
|
45
66
|
urllib3==2.2.2
|
|
67
|
+
wrapt==1.16.0
|
|
46
68
|
yarl==1.9.2
|
|
47
69
|
zipp==3.17.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|