Compare commits

15 Commits

Author SHA1 Message Date
6030f2d0dd Split up add params 2024-04-12 12:53:39 -04:00
c7782b46b8 Delete config.ini 2024-04-10 20:53:28 +00:00
5772f1be4b Now actually read the config file. 😅 2024-04-10 16:49:00 -04:00
d3a222e27b Better way to handle logging 2024-04-10 16:44:57 -04:00
5dd3e92a5b Formatting & Refactoring 2024-04-10 13:35:42 -04:00
0d53c8c292 Added notes on what to work on 2024-04-10 12:29:57 -04:00
1ce273bc61 Disable broken logging 2024-04-08 16:46:18 -04:00
65efcbb171 Removed unused conditonals 2024-04-08 16:44:39 -04:00
e07dd0c5cd Now read the channel list from the config.ini 2024-04-08 16:34:28 -04:00
33d7ae24fb More clean up with type hints 2024-04-08 12:56:56 -04:00
aaf2173894 More clean up with type hints 2024-04-08 12:49:28 -04:00
4218dc827f Clean up 2024-04-07 15:57:03 -04:00
660b7f1e82 Clean up 2024-04-07 15:55:49 -04:00
ed2f65bee5 Should maybe make requests a requirement 2024-04-06 23:47:07 -04:00
18c0f2123d Fixed stream saved location 2024-04-06 22:44:51 -04:00
5 changed files with 135 additions and 77 deletions

4
.gitignore vendored
View File

@ -159,8 +159,8 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# Channel List # Configuration
channel_list.txt config.ini
# dl-stream log # dl-stream log
log.txt log.txt

View File

@ -2,12 +2,52 @@
<!--toc:start--> <!--toc:start-->
- [Auto TTV Grabber](#auto-ttv-grabber) - [Auto TTV Grabber](#auto-ttv-grabber)
- [Getting started](#getting-started) - [tl;dr](#tldr)
- [Configuration](#configuration)
- [Settings](#settings)
- [Adding streams](#adding-streams)
<!--toc:end--> <!--toc:end-->
## Getting started ## tl;dr
This script relies on [streamlink](https://streamlink.github.io) being in your systems path. This script relies on [streamlink](https://streamlink.github.io) being installed.
You can find all of its requirements on the their project page. You can find all of its requirements on the their project page.
This script loops through all of the channels in `channel_list.txt` once per minute checking to see if the channel is live via a HTTP request. This script loops through all of the channels in `config.ini`,
Once a channel is live a [streamlink](https://streamlink.github.io) subprocess spawns in the background downloading the stream to it's default location (`$HOME/Videos/Stream/<channel_name>`) once per minute checking to see if the channel is live via a HTTP request.
Once a channel is live a `streamlink` subprocess spawns in the background,
downloading the stream to it's default location (`$HOME/Downloads/Stream/<channel_name>`)
If you wish to change this location, you can find out how to below
## Configuration
You can copy `config.ini.example` from the repo to `config.ini` to begin.
Leaving a setting blank will render it being ignored and using the default value.
### Settings
- `streamlink_location`: If `streamlink` is not in your path you can specify
its absolute location here. Otherwise it looks for it in your path.
- `download_location`: You can specify the absolute path to where you would like
to save the steams. A subdirectory will be created for each channel as it goes live.
If left blank the default location is: `$HOME/Downloads/Streams/<channel_name>`
- `skip_ads` : Takes a `True` or `False` value. If `True` you will need to have
installed the [ttvlol](https://github.com/2bc4/streamlink-ttvlol) `streamlink`
plugin. Setting this to `True` without doing so will cause the
`streamlink` subprocess to fail. Default value is `False`
- `log` : Takes a `True` or `False` value. If `True` there will be a `log.txt` file
in the channel's download directory with all of the output that came from its
corresponding subprocess. Default value is `False`
### Adding streams
You will need to add the channels Twitch username to `config.ini`:
```ini
[streams]
1 = coney
2 = dougdoug
3 = parkzer
```
As for as I know there is no limit to how many streams you can add this way,
just make sure when you add a new stream it is incremented by one

View File

@ -2,4 +2,7 @@
streamlink_location = "" streamlink_location = ""
download_location = "" download_location = ""
skip_ads = skip_ads =
log =
[streams]

148
main.py
View File

@ -7,58 +7,78 @@ import sys
import time import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import List
import requests import requests
channel_list = [] channel_list: List[str] = []
downloading = {} downloading = {}
# Default Config Settings # Default Config Settings
config = configparser.ConfigParser() streamlink_location: str = "streamlink"
streamlink_location = "streamlink" download_location: str = f"{Path.home()}/Downloads/Streams"
download_location = f"{Path.home()}/Downloads/Streams" skip_ads: bool = False
skip_ads = False log: bool = False
def load_config(): def load_config() -> None:
print("Reading config file...") print("Reading config file...")
if os.path.exists("config.ini"): config = configparser.ConfigParser()
config.read("config.ini") config.read("config.ini")
if ( if config.has_option("settings", "streamlink_location"):
config.has_option("settings", "streamlink_location") streamlink_location = config["settings"]["streamlink_location"]
and not config["settings"]["streamlink_location"].strip() print(f"Streamlink location: {streamlink_location}")
): if config.has_option("settings", "download_location"):
streamlink_location = config["settings"]["streamlink_location"] download_location = config["settings"]["download_location"]
if ( print(f"Download location: {download_location}")
config.has_option("settings", "download_location") if config.has_option("settings", "skip_ads"):
and not config["settings"]["download_location"].strip() skip_ads = bool(config["settings"]["skip_ads"])
): print(f"Skip ads: {skip_ads}")
download_location = config["settings"]["download_location"] if config.has_option("settings", "log"):
if ( log = bool(config["settings"]["log"])
config.has_option("settings", "skip_ads") print(f"Logs: {log}")
and not config["settings"]["skip_ads"]
): if len(config["streams"]) < 1:
skip_ads = bool(config["settings"]["skip_ads"]) sys.exit("ERROR: No streams found in config.ini! See README.md for more info.")
print("Config file loaded")
else: else:
print("No config file found using default values!") for index in range(1, len(config["streams"]) + 1):
channel_list.append(config["streams"][str(index)])
print("Config file loaded")
def write_log(channel): # TODO: Have steamlink itself check if the channel is live
"""Writes the latest stdout of a process to log.txt""" def is_live(channel: str) -> bool:
with open("log.txt", "a") as log: """Checks if a channel is live on Twitch"""
line = downloading[channel].stdout.readline() try:
if line: contents = requests.get("https://www.twitch.tv/" + channel).content.decode(
log.write(line.decode()) "utf-8"
)
if "isLiveBroadcast" in contents:
return True
else:
return False
except Exception:
print(f"There was an issue checking if {channel} was life. Will try next time!")
if channel in downloading:
return True
else:
return False
def download_stream(channel): def download_stream(channel: str) -> None:
"""Downloads a given channel name in its own subprocess""" """Downloads a given channel name in its own subprocess"""
# TODO: Just clean this up at somepoint # TODO: Just clean this up at somepoint
addtional_parms = "" loging: str = ""
ad_skipping: str = ""
print(log)
if log:
loging = f"--loglevel info --logfile {download_location}/{channel}/log.txt"
if skip_ads: if skip_ads:
addtional_parms = "--twitch-proxy-playlist=https://lb-eu.cdn-perfprod.com,https://lb-eu2.cdn-perfprod.com,https://lb-na.cdn-perfprod.com,https://lb-as.cdn-perfprod.com,https://as.luminous.dev --twitch-disable-ads" ad_skipping = "--twitch-proxy-playlist=https://lb-eu.cdn-perfprod.com,https://lb-eu2.cdn-perfprod.com,https://lb-na.cdn-perfprod.com,https://lb-as.cdn-perfprod.com,https://as.luminous.dev --twitch-disable-ads"
file_name = f"{channel}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.ts" file_name: str = f"{channel}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.ts"
cmd = f"{streamlink_location} --loglevel none --retry-max 10 {addtional_parms} -o {download_location}/{channel}/{file_name} twitch.tv/{channel} best" cmd: str = f"{streamlink_location} --retry-max 10 {loging} {ad_skipping} -o {download_location}/{channel}/{file_name} twitch.tv/{channel} best"
# TODO: Check if the process failed for some reason
downloading[channel] = subprocess.Popen( downloading[channel] = subprocess.Popen(
[cmd], [cmd],
shell=True, shell=True,
@ -66,23 +86,19 @@ def download_stream(channel):
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
) )
write_log(channel)
def check_system(): def check_system() -> None:
"""Makes sure everything is place for the script to run""" """Makes sure everything is place for the script to run"""
# Checks for config file
if not os.path.exists("config.ini"):
sys.exit("ERROR: config.ini is not found! See README.md for more info.")
# Checks if streamlink is in the systems path # Checks if streamlink is in the systems path
if not shutil.which("streamlink"): if not shutil.which("streamlink"):
sys.exit("ERROR: streamlink is not found in the systems path!") sys.exit("ERROR: streamlink is not found in the systems path!")
# TODO: Combine the channel_list into the config
# Checks if the channel_list exists and if not makes one
if not os.path.exists("channel_list.txt"):
print("ERROR:'channel_list.txt' does not exist, creating now!")
with open("channel_list.txt", "w"):
pass # Creates empty file
sys.exit("Please populate the channel_list.txt with one channel per line!")
# Make sure the download location exists # Make sure the download location exists
if not os.path.exists(download_location): if not os.path.exists(download_location):
print( print(
@ -91,7 +107,7 @@ def check_system():
os.makedirs(download_location) os.makedirs(download_location)
def stop_downloads(): def stop_downloads() -> None:
"""Goes through every process and stops it if running""" """Goes through every process and stops it if running"""
print("\nCleaning up...") print("\nCleaning up...")
for name, proc in downloading.items(): for name, proc in downloading.items():
@ -99,52 +115,46 @@ def stop_downloads():
print(f"Stopping download of {name}") print(f"Stopping download of {name}")
def main(): def main() -> None:
"""Main entry point of the app""" """Main entry point of the app"""
# Grab all the channels from channel_list.txt and put them in a list
with open("channel_list.txt", "r") as file:
# channel_list = file.readlines()
channel_list = [
line for line in file if line.strip()
] # Removes all white spaces per line
# Run untill progam is killed # Run untill progam is killed
# TODO: Check on the process if it is still alive and restart as needed
while True: while True:
# Exits the program if there is no channels to grab # Exits the program if there is no channels to grab
if not channel_list:
sys.exit("Please populate the channel_list.txt with one channel per line!")
print("\n------------------------------------") print("\n------------------------------------")
for channel in channel_list: for channel in channel_list:
channel = channel.strip() channel = channel.strip()
# TODO: Have steamlink itself check if the channel is live
contents = requests.get("https://www.twitch.tv/" + channel).content.decode( if is_live(channel):
"utf-8" print(f"\n\033[1m{channel}\033[0m is \033[32mlive\033[0m!", end=" ")
)
if "isLiveBroadcast" in contents:
print(f"\033[1m{channel}\033[0m is \033[32mlive\033[0m!")
if channel not in downloading: if channel not in downloading:
download_stream(channel) download_stream(channel)
else: else:
print(f"{channel} is already downloading") print("\033[33m(Already Downloading)\033[0m")
write_log(channel)
else: else:
print("\033[1m" + channel + "\033[0m is \033[31mnot live\033[0m.") print(
"\n\033[1m" + channel + "\033[0m is \033[31mnot live\033[0m.",
end=" ",
)
if channel in downloading: if channel in downloading:
del downloading[channel] del downloading[channel]
print(f"{channel} is no longer downloading") print("\033[35m(Stopping)\033[0m")
time.sleep(1) # Wait one second before going to next channel time.sleep(1) # Wait one second before going to next channel
print( print(
"\n\033[3mLast checked: " + datetime.now().strftime("%H:%M:%S") + "\033[0m" "\n\n\033[3mLast checked: "
+ datetime.now().strftime("%H:%M:%S")
+ "\033[0m"
) )
print("------------------------------------") print("------------------------------------")
time.sleep(60) # Wait 60 Seconds before trying again time.sleep(60) # Wait 60 Seconds before trying again
if __name__ == "__main__": if __name__ == "__main__":
""" This is executed when run from the command line """ """This is executed when run from the command line"""
try: try:
load_config()
check_system() check_system()
load_config()
main() main()
finally: finally:
stop_downloads() stop_downloads()

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
certifi==2024.2.2
charset-normalizer==3.3.2
idna==3.6
requests==2.31.0
urllib3==2.2.1