main.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import os
  2. import time
  3. import datetime
  4. import pickle
  5. import threading
  6. from googleapiclient.discovery import build
  7. import subprocess
  8. from google.auth.transport.requests import Request
  9. import sys
  10. from auth import get_token, get_token_non_interactive, get_service_account_token
  11. # Constants
  12. OUTPUT_DIR = "/media/media/YouTube" # Change this to your Emby folder
  13. DAYS_BACK = 5 # Videos published in the last 5 days
  14. DAYS_TO_KEEP = 10 # Delete videos older than 10 days
  15. TOKEN_FILE = "youtube_token.pickle" # Token file from the OAuth script
  16. EXIT_TIME = datetime.time(6, 0) # Exit the script at 06:00
  17. POPULAR_VIDEOS_MAX_RESULTS = 10 # Number of popular videos to fetch (you can change this)
  18. REGION_CODES = ["US", "GB", "LU", "LK"] # Regions to fetch popular videos from
  19. def update_yt_dlp():
  20. """Update yt-dlp to the latest version."""
  21. print("Updating yt-dlp to the latest version...")
  22. subprocess.run(["pip", "install", "--upgrade", "yt-dlp"], check=True)
  23. print("yt-dlp updated successfully!")
  24. def authenticate_youtube_api():
  25. """Authenticate using our enhanced auth system (tries non-interactive first)."""
  26. print("Attempting non-interactive authentication...")
  27. # Try non-interactive authentication first
  28. creds = get_token_non_interactive()
  29. if not creds:
  30. print("Non-interactive authentication failed.")
  31. print("You need to run: python3 auth.py --interactive")
  32. print("Or set up service account authentication.")
  33. raise Exception("Authentication failed. No valid credentials available.")
  34. print("Authentication successful!")
  35. # Build the YouTube API client
  36. youtube = build("youtube", "v3", credentials=creds)
  37. return youtube
  38. def authenticate_with_token():
  39. """Legacy function - kept for compatibility but redirects to new auth system."""
  40. return authenticate_youtube_api()
  41. def get_subscribed_channels(youtube):
  42. """Fetch the list of subscribed channels."""
  43. channels = []
  44. request = youtube.subscriptions().list(
  45. part="snippet",
  46. mine=True,
  47. maxResults=50
  48. )
  49. while request:
  50. response = request.execute()
  51. for item in response["items"]:
  52. channels.append(item["snippet"]["resourceId"]["channelId"])
  53. request = youtube.subscriptions().list_next(request, response)
  54. return channels
  55. def get_recent_videos(youtube, channel_id):
  56. """Fetch videos from a channel published in the last `DAYS_BACK` days."""
  57. recent_videos = []
  58. published_after = (datetime.datetime.now() - datetime.timedelta(days=DAYS_BACK)).isoformat("T") + "Z"
  59. request = youtube.search().list(
  60. part="snippet",
  61. channelId=channel_id,
  62. publishedAfter=published_after,
  63. maxResults=50,
  64. type="video",
  65. order="date"
  66. )
  67. response = request.execute()
  68. for item in response["items"]:
  69. recent_videos.append({
  70. "video_id": item["id"]["videoId"],
  71. "title": item["snippet"]["title"],
  72. "description": item["snippet"]["description"],
  73. "published_at": item["snippet"]["publishedAt"],
  74. "channel_title": item["snippet"]["channelTitle"]
  75. })
  76. return recent_videos
  77. def get_popular_videos(youtube, region_code):
  78. """Fetch the most popular videos for a given region."""
  79. popular_videos = []
  80. request = youtube.videos().list(
  81. part="snippet,statistics",
  82. chart="mostPopular",
  83. regionCode=region_code, # Region code passed here
  84. maxResults=POPULAR_VIDEOS_MAX_RESULTS
  85. )
  86. response = request.execute()
  87. for item in response["items"]:
  88. popular_videos.append({
  89. "video_id": item["id"],
  90. "title": item["snippet"]["title"],
  91. "description": item["snippet"]["description"],
  92. "published_at": item["snippet"]["publishedAt"],
  93. "channel_title": item["snippet"]["channelTitle"]
  94. })
  95. return popular_videos
  96. def download_video(video_id, output_path):
  97. """Download a YouTube video prioritizing 4K non-AV1 formats, with fallback options."""
  98. try:
  99. command = [
  100. "yt-dlp",
  101. "-f", "bv[height=2160][vcodec!*=av1]+ba / bv[height<=1080]+ba / best",
  102. "--merge-output-format", "mp4",
  103. "--no-mtime",
  104. f"https://www.youtube.com/watch?v={video_id}",
  105. "-o", os.path.join(output_path, "%(title)s.%(ext)s")
  106. ]
  107. subprocess.run(command, check=True)
  108. print(f"Successfully downloaded video {video_id}")
  109. except subprocess.CalledProcessError as e:
  110. print(f"Error downloading video {video_id}: {e}")
  111. return False
  112. return True
  113. # Temporarily commented out video conversion functionality
  114. # def convert_to_mp4(input_file):
  115. # """Convert a video to mp4 and delete the original file."""
  116. # mp4_file = os.path.splitext(input_file)[0] + ".mp4"
  117. # print(f"Converting {input_file} to {mp4_file}...")
  118. # command = ["ffmpeg", "-i", input_file, "-c:v", "libx264", "-preset", "fast", "-c:a", "aac", mp4_file]
  119. # subprocess.run(command, check=True)
  120. # print(f"Conversion completed: {mp4_file}")
  121. # if os.path.exists(mp4_file):
  122. # os.remove(input_file)
  123. # print(f"Original file deleted: {input_file}")
  124. def create_nfo_file(video_data, file_path):
  125. """Create an Emby-compatible NFO file."""
  126. nfo_content = f"""<?xml version="1.0" encoding="utf-8" standalone="yes"?>
  127. <movie>
  128. <title>{video_data['title']}</title>
  129. <plot>{video_data['description']}</plot>
  130. <year>{video_data['published_at'][:4]}</year>
  131. <premiered>{video_data['published_at']}</premiered>
  132. <studio>{video_data['channel_title']}</studio>
  133. </movie>
  134. """
  135. with open(file_path, "w", encoding="utf-8") as nfo_file:
  136. nfo_file.write(nfo_content)
  137. def delete_old_videos(folder, days=5):
  138. """Delete video files and their folders older than `days`."""
  139. now = time.time()
  140. cutoff = now - (days * 86400) # Convert days to seconds
  141. for root, dirs, files in os.walk(folder, topdown=False):
  142. # Delete files
  143. for file in files:
  144. file_path = os.path.join(root, file)
  145. if os.path.isfile(file_path):
  146. if os.path.getmtime(file_path) < cutoff:
  147. print(f"Deleting file: {file_path}")
  148. os.remove(file_path)
  149. # Remove empty directories
  150. for dir in dirs:
  151. dir_path = os.path.join(root, dir)
  152. if not os.listdir(dir_path): # Check if the directory is empty
  153. print(f"Deleting folder: {dir_path}")
  154. os.rmdir(dir_path)
  155. def video_exists(output_path, video_id):
  156. """Check if a video already exists in the directory."""
  157. return os.path.exists(os.path.join(output_path, video_id))
  158. def check_exit_time():
  159. """Check if the current time is past the exit time."""
  160. now = datetime.datetime.now().time()
  161. if now >= EXIT_TIME:
  162. print("Current time exceeds exit time. Terminating script.")
  163. sys.exit(0)
  164. def main():
  165. # Update yt-dlp
  166. update_yt_dlp()
  167. # Authenticate YouTube API
  168. print("Authenticating with YouTube API...")
  169. youtube = authenticate_youtube_api()
  170. if not os.path.exists(OUTPUT_DIR):
  171. os.makedirs(OUTPUT_DIR)
  172. # Clean up old videos
  173. print(f"Deleting videos older than {DAYS_TO_KEEP} days in {OUTPUT_DIR}...")
  174. delete_old_videos(OUTPUT_DIR, days=DAYS_TO_KEEP)
  175. # Fetch subscribed channels
  176. print("Fetching subscribed channels...")
  177. channels = get_subscribed_channels(youtube)
  178. # Fetch and process recent videos from subscribed channels
  179. for channel_id in channels:
  180. print(f"Fetching recent videos for channel: {channel_id}")
  181. videos = get_recent_videos(youtube, channel_id)
  182. for video in videos:
  183. # Check the exit time before processing each video
  184. check_exit_time()
  185. print(f"Processing video: {video['title']}")
  186. video_dir = os.path.join(OUTPUT_DIR, video["video_id"])
  187. # Check if the video already exists
  188. if os.path.exists(video_dir):
  189. print(f"Skipping download, already exists: {video['title']} ({video['video_id']})")
  190. continue
  191. os.makedirs(video_dir, exist_ok=True)
  192. # Download the video
  193. if not download_video(video["video_id"], video_dir):
  194. print(f"Skipping video {video['video_id']} due to download error.")
  195. continue # Skip this video if download failed
  196. # Temporarily comment out conversion, but keep it in the code for future use
  197. # video_file = next(
  198. # (f for f in os.listdir(video_dir) if not f.endswith(".mp4")),
  199. # None
  200. # )
  201. # if video_file:
  202. # input_path = os.path.join(video_dir, video_file)
  203. # threading.Thread(target=convert_to_mp4, args=(input_path,)).start()
  204. # Create the NFO file
  205. nfo_file_path = os.path.join(video_dir, "movie.nfo")
  206. create_nfo_file(video, nfo_file_path)
  207. # Fetch and process most popular videos worldwide (from multiple regions)
  208. for region_code in REGION_CODES:
  209. print(f"Fetching the most popular videos from {region_code}...")
  210. popular_videos = get_popular_videos(youtube, region_code)
  211. for video in popular_videos:
  212. # Check the exit time before processing each video
  213. check_exit_time()
  214. print(f"Processing popular video: {video['title']} ({region_code})")
  215. video_dir = os.path.join(OUTPUT_DIR, video["video_id"])
  216. # Check if the video already exists
  217. if os.path.exists(video_dir):
  218. print(f"Skipping download, already exists: {video['title']} ({video['video_id']})")
  219. continue
  220. os.makedirs(video_dir, exist_ok=True)
  221. # Download the popular video
  222. if not download_video(video["video_id"], video_dir):
  223. print(f"Skipping popular video {video['video_id']} due to download error.")
  224. continue # Skip this video if download failed
  225. # Temporarily comment out conversion, but keep it in the code for future use
  226. # video_file = next(
  227. # (f for f in os.listdir(video_dir) if not f.endswith(".mp4")),
  228. # None
  229. # )
  230. # if video_file:
  231. # input_path = os.path.join(video_dir, video_file)
  232. # threading.Thread(target=convert_to_mp4, args=(input_path,)).start()
  233. # Create the NFO file
  234. nfo_file_path = os.path.join(video_dir, "movie.nfo")
  235. create_nfo_file(video, nfo_file_path)
  236. print("All videos processed successfully!")
  237. if __name__ == "__main__":
  238. main()