initial commit
This commit is contained in:
		
						commit
						d4055ac0c1
					
				
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					__pycache__
 | 
				
			||||||
 | 
					venv/
 | 
				
			||||||
 | 
					**/*.mp4
 | 
				
			||||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					# FAU-TV Video Downloader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Downloads all videos of a given course (even if the download has been restricted) to a local folder.
 | 
				
			||||||
 | 
					This software is provided without warranty. Usage is discouraged! Usage of this software is at your own risk!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					./dl '<fau course id>' '[output directory]' '[starter url]'
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Procedure
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Upon starting, a firefox instance is opened, prompting you to log in using IDM SSO. As soon as the url changes to something starting with `https://www.fau.tv`, download will be starting. If you do not want to provide your credentials, you can provide `https://www.fau.tv` as a starter url.
 | 
				
			||||||
							
								
								
									
										21
									
								
								dl
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										21
									
								
								dl
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					from .lib import *
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    course_id = sys.argv[1]
 | 
				
			||||||
 | 
					    out_dir = sys.argv[2] if len(sys.argv) >= 3 else './out'
 | 
				
			||||||
 | 
					    auth_url = sys.argv[3] if len(sys.argv) >= 4 else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    load_token(auth_url) if auth_url is not None else load_token()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    os.makedirs(out_dir, exist_ok=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for index, clip_id in enumerate(get_course_clip_ids(course_id)):
 | 
				
			||||||
 | 
					        print(f'downloading clip {clip_id}')
 | 
				
			||||||
 | 
					        download_clip(clip_id, f'{out_dir}/{index+1: 04d}_{clip_id}.mp4')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
							
								
								
									
										154
									
								
								lib.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								lib.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,154 @@
 | 
				
			|||||||
 | 
					from selenium import webdriver
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_token: "Token" = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class Token():
 | 
				
			||||||
 | 
					    auth_token: str
 | 
				
			||||||
 | 
					    session_id: str
 | 
				
			||||||
 | 
					    session_ci: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def cookies(self):
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            "SimpleSAMLAuthToken": self.auth_token,
 | 
				
			||||||
 | 
					            "SimpleSAMLSessionID": self.session_id,
 | 
				
			||||||
 | 
					            "session_ci": self.session_ci,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class ClipDetails():
 | 
				
			||||||
 | 
					    combined_media_id: str = None
 | 
				
			||||||
 | 
					    combined_playlist_url: str = None
 | 
				
			||||||
 | 
					    camera_media_id: str = None
 | 
				
			||||||
 | 
					    camera_playlist_url: str = None
 | 
				
			||||||
 | 
					    slides_media_id: str = None
 | 
				
			||||||
 | 
					    slides_playlist_url: str = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def load_token(auth_url: str = "https://www.fau.tv/auth/sso"):
 | 
				
			||||||
 | 
					    global _token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    driver = webdriver.Firefox()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    driver.get(auth_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while not driver.current_url.startswith('https://www.fau.tv/'):
 | 
				
			||||||
 | 
					        time.sleep(0.5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_value(cookie):
 | 
				
			||||||
 | 
					        if cookie is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        return cookie.get("value")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _token = Token(
 | 
				
			||||||
 | 
					        auth_token=get_value(driver.get_cookie("SimpleSAMLAuthToken")),
 | 
				
			||||||
 | 
					        session_id=get_value(driver.get_cookie("SimpleSAMLSessionID")),
 | 
				
			||||||
 | 
					        session_ci=get_value(driver.get_cookie("session_ci")),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print(_token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    driver.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_course_clip_ids(course_id: str) -> list[str]:
 | 
				
			||||||
 | 
					    global _token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    regex = re.compile(r'(/clip/id/)([0-9]+)(\"\s*class=\"preview\")')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    urls = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    url = f'https://www.fau.tv/course/id/{course_id}'
 | 
				
			||||||
 | 
					    with requests.get(url, cookies=_token.cookies()) as r:
 | 
				
			||||||
 | 
					        clip_matches = regex.findall(r.text)
 | 
				
			||||||
 | 
					        for match in clip_matches:
 | 
				
			||||||
 | 
					            urls.append(match[1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_clip_details(clip_id: str) -> ClipDetails:
 | 
				
			||||||
 | 
					    global _token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    url = f'https://www.fau.tv/clip/id/{clip_id}'
 | 
				
			||||||
 | 
					    details = ClipDetails()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with requests.get(url, cookies=_token.cookies()) as r:
 | 
				
			||||||
 | 
					        def get_details(keyword: str):
 | 
				
			||||||
 | 
					            mediaid_re = re.compile(
 | 
				
			||||||
 | 
					                r'(' + keyword + r'Sources[^,]*,\s+mediaid\:\s+\")([0-9]+)'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            playlist_url_re = re.compile(
 | 
				
			||||||
 | 
					                r'(file\:\s+\")([^\"]*' + keyword + r'\.smil[^\"]*)(\")'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            mediaid_matches = mediaid_re.findall(r.text)
 | 
				
			||||||
 | 
					            if len(mediaid_matches) > 0:
 | 
				
			||||||
 | 
					                setattr(details, keyword + "_media_id", mediaid_matches[0][1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            playlist_url_matches = playlist_url_re.findall(r.text)
 | 
				
			||||||
 | 
					            if len(playlist_url_matches) > 0:
 | 
				
			||||||
 | 
					                setattr(details, keyword + "_playlist_url",
 | 
				
			||||||
 | 
					                        playlist_url_matches[0][1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        get_details("combined")
 | 
				
			||||||
 | 
					        get_details("camera")
 | 
				
			||||||
 | 
					        get_details("slides")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return details
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def download_media(media_id: str, outfile_path: str):
 | 
				
			||||||
 | 
					    global _token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    url = f'https://itunes.video.uni-erlangen.de/get/file/' + \
 | 
				
			||||||
 | 
					        str(media_id) + '?download=1'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with requests.get(url, stream=True, cookies=_token.cookies()) as r:
 | 
				
			||||||
 | 
					        if (r.status_code != 200):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with open(outfile_path, 'wb') as f:
 | 
				
			||||||
 | 
					            shutil.copyfileobj(r.raw, f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def download_playlist(playlist_url: str, outfile_path: str):
 | 
				
			||||||
 | 
					    subprocess.run([
 | 
				
			||||||
 | 
					        'ffmpeg',
 | 
				
			||||||
 | 
					        '-i', playlist_url,
 | 
				
			||||||
 | 
					        '-c', 'copy',
 | 
				
			||||||
 | 
					        outfile_path,
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def download_clip(clip_id: str, outfile_path: str):
 | 
				
			||||||
 | 
					    details = get_clip_details(clip_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    media_id = next(id for id in [
 | 
				
			||||||
 | 
					        details.combined_media_id,
 | 
				
			||||||
 | 
					        details.camera_media_id,
 | 
				
			||||||
 | 
					        details.slides_media_id,
 | 
				
			||||||
 | 
					    ] if id is not None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if download_media(media_id, outfile_path):
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    url = next(url for url in [
 | 
				
			||||||
 | 
					        details.combined_playlist_url,
 | 
				
			||||||
 | 
					        details.camera_playlist_url,
 | 
				
			||||||
 | 
					        details.slides_playlist_url,
 | 
				
			||||||
 | 
					    ] if url is not None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    download_playlist(url, outfile_path)
 | 
				
			||||||
							
								
								
									
										5
									
								
								pyvenv.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								pyvenv.cfg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					home = /usr/bin
 | 
				
			||||||
 | 
					include-system-site-packages = false
 | 
				
			||||||
 | 
					version = 3.12.4
 | 
				
			||||||
 | 
					executable = /usr/bin/python3.12
 | 
				
			||||||
 | 
					command = /usr/bin/python -m venv /home/ludwig/git/fau-tv-dl
 | 
				
			||||||
							
								
								
									
										2
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					selenium
 | 
				
			||||||
 | 
					requests
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user