Last active
May 6, 2024 17:39
-
-
Save CrazySqueak/4f914bd2188663f5f4a68146f3d0e665 to your computer and use it in GitHub Desktop.
Extracts a playlist from VLC for Android, and writes it to an m3u8 file. Follow usage instructions at the top of the file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# By Anabelle Page-John, 2024 | |
# | |
# Installation: | |
# 1. Download this file to your phone. | |
# 2. Install termux from the Google Play Store or F-Droid (if you have it). | |
# 3. In termux, type "pkg install python3" and press ENTER to install python. | |
# | |
# Usage: | |
# In VLC, go to Advanced and select Dump Media Database | |
# which will dump the VLC database to your storage | |
# Then, in termux (or similar), run this using python3 ( python3 ~/storage/downloads/extract_vlc_android_playlist.py ) | |
# and select options by typing the numbers (or answering y/n) when asked. | |
# | |
# This will extract the playlist data from VLC For Android, and create | |
# an m3u8 file, which does not randomly delete items for no reason | |
# (unlike VLC for Android's standard playlist format, which does) | |
# | |
DB_PATH="/storage/emulated/0/vlc_media.db" | |
import sqlite3, sys, os, functools | |
db = sqlite3.connect(DB_PATH) | |
#db.row_factory = sqlite3.Row | |
cur = db.cursor() | |
print("VLC for Android - Playlist Extractor") | |
print("Because their default playlist handling is a massive pain(tm).") | |
print() | |
playlist_names = {} | |
print("Select Playlist to Extract:") | |
for playlist_id, playlist_name, nb_v, nb_a, nb_u in cur.execute("SELECT Playlist.id_playlist, Playlist.name, Playlist.nb_video, Playlist.nb_audio, Playlist.nb_unknown FROM 'Playlist' ORDER BY Playlist.id_playlist ASC;"): | |
if (nb_v+nb_a+nb_u) == 0: continue # Skip empty | |
playlist_names[playlist_id] = playlist_name | |
print(f"{playlist_id}. {playlist_name} ({nb_v} videos, {nb_a} audios, {nb_u} unknowns)") | |
playlist_id = int(input("Select playlist: ")) | |
print() | |
print("Listing tracks...") | |
for idx, title in cur.execute("SELECT PlaylistMediaRelation.position, Media.title FROM 'Media', 'PlaylistMediaRelation' WHERE Media.id_media = PlaylistMediaRelation.media_id AND PlaylistMediaRelation.playlist_id = ? ORDER BY PlaylistMediaRelation.position ASC", (playlist_id,)): | |
print(f"{idx}. {title}") | |
while True: | |
yn = input("Extract this playlist?[y/n] ").lower() | |
if yn and yn in "yn": break | |
if yn == "n": | |
print("Exit.") | |
sys.exit() | |
print() | |
SAVE_FOLDERS = [ | |
(None, "Custom"), | |
("~", "Termux Home"), | |
("~/storage/music", "Music"), | |
("~/storage/downloads", "Downloads"), | |
("~/storage/shared", "Storage Root"), | |
("/storage/emulated/0/Music", "Music"), | |
("/storage/emulated/0/Download", "Downloads"), | |
("/storage/emulated/0/", "Storage Root"), | |
] | |
#if os.environ.get("TERMUX_VERSION") is not None: # Running in termux | |
# SAVE_FOLDERS.extend([ | |
# ]) | |
#else: # Running in base android | |
# SAVE_FOLDERS.extend([ | |
# ]) | |
print("Select Folder To Save To:") | |
for i,(path,comment) in enumerate(SAVE_FOLDERS): | |
if i == 0: continue | |
if not os.path.exists(os.path.expanduser(path)): continue # Not found | |
print(f"{i} - {path} ({comment})") | |
print("0 - Custom") | |
target = int(input("Select an option: ")) | |
if target == 0: | |
path = input("Enter Custom Path: ") | |
else: | |
path = SAVE_FOLDERS[target][0] | |
# Select filename | |
filename = playlist_names[playlist_id] | |
filename = "".join((c if c.isalnum() else "_" for c in filename)) | |
# Remove duplicate _s | |
changed = True | |
while changed: | |
changed = False | |
for i in range(len(filename)-1): | |
if filename[i] == "_" and filename[i+1] == "_": | |
filename = filename[:i] + filename[i+1:] # Remove underscore | |
changed = True | |
break | |
filename += ".m3u8" | |
playlist_directory = path | |
path = os.path.join(playlist_directory, filename) | |
print("Selected path:", path) | |
playlist_directory = os.path.realpath(os.path.expanduser(playlist_directory)) # Resolve symlinks | |
path = os.path.expanduser(path) | |
if os.path.exists(path): | |
while True: | |
yn = input("File already exists. Overwrite?[y/n] ").lower() | |
if yn and yn in "yn": break | |
if yn == "n": | |
print("Aborting.") | |
sys.exit() | |
# https://datatracker.ietf.org/doc/html/rfc8216 (technically for HLS but it's a superset of extended MU3 so still relevant) | |
# https://en.wikipedia.org/wiki/M3U#M3U8 | |
FILE_PFX = "file://" | |
print("Writing playlist...") | |
with open(path, "w", encoding="utf-8", newline="\r\n") as f: | |
fprint = functools.partial(print, file=f) | |
fprint("#EXTM3U") | |
fprint("#EXTENC:UTF-8") | |
fprint(f"#PLAYLIST:{playlist_names[playlist_id]}") | |
for mrl, title, duration in cur.execute("SELECT File.mrl, Media.title, Media.duration FROM 'Media', 'PlaylistMediaRelation', 'File' WHERE Media.id_media = PlaylistMediaRelation.media_id AND Media.id_media = File.media_id AND PlaylistMediaRelation.playlist_id = ? ORDER BY PlaylistMediaRelation.position ASC", (playlist_id,)): | |
if duration == -1: duration = 0 | |
fprint(f"#EXTINF:{int(duration/1000)},{title if title else ''}") # (convert duration from ms to s) | |
if mrl.startswith(FILE_PFX): | |
media_path = os.path.realpath(mrl.replace(FILE_PFX,"")) | |
media_path = os.path.relpath(media_path, start=playlist_directory) | |
else: | |
media_path = mrl # Fuck knows O.O | |
fprint(media_path) | |
print("Done :)") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment