Skip to content

Instantly share code, notes, and snippets.

@CrazySqueak
Last active May 6, 2024 17:39
Show Gist options
  • Save CrazySqueak/4f914bd2188663f5f4a68146f3d0e665 to your computer and use it in GitHub Desktop.
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.
#!/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