#!/usr/bin/env python3
# musicarray - make a playlist for Nokia 225
# usage: musicarray 'Album Title' mount_path listinfo_path
# Makes a playlist at 'mount_path\System\Mp3_res\Album Title.lst',
# tracks taken from the text file album_title/playlist.txt,
# assuming the tracks are in E:\Music files\at\, and adds
# a reference to the playlist in listinfo_path.
# Mainly developed 2025-226
#
# https://dkl9.net/essays/musicarray_format.html

import sys
import struct
import ntpath
from typing import Optional
from mutagen import File

def encode_utf16_like(text: str, max_bytes: int) -> bytes:
    """
    Encodes text as UTF-16-like (ASCII char followed by null byte).
    Pads with null bytes to reach max_bytes length.
    """
    encoded = b''
    for char in text:
        code_point = ord(char)
        encoded += struct.pack('<H', code_point if code_point < 256 else 63)
    encoded = encoded.ljust(max_bytes, b'\x00')
    return encoded[:max_bytes]

def create_binary_field(duration_seconds: int, path_length: int) -> bytes:
    """
    Creates the 32-byte binary field based on the template pattern.
    """
    # from example of "01 Overture.mp3"
    field = bytearray(bytes.fromhex(
        '22000d08e907000016101700f8dd510050010000000000000000000030303030'
    ))
    field[0:2] = struct.pack('<H', path_length)
    field[16:18] = struct.pack('<H', duration_seconds)
    return bytes(field)

def get_file_duration(filepath: str) -> int:
    """
    Attempts to get file duration. Returns 0 if unable to determine.
    """
    audio_file = File(filepath)
    return int(audio_file.info.length) if audio_file else 0

def extract_title_from_filename(filename: str) -> str:
    """
    Extracts title from filename, removing extension and path.
    """
    return ntpath.splitext(ntpath.basename(filename))[0]

def create_playlist_record(
    filepath: str,
    duration: Optional[int] = None,
    title: Optional[str] = None,
    directory: Optional[str] = None,
) -> bytes:
    """
    Creates a single playlist record (788 bytes total).
    """
    if duration is None:
        duration = get_file_duration(
            (directory + "/" if directory else "") + ntpath.basename(filepath)
        )
    if title is None:
        title = extract_title_from_filename(filepath)
    filename_field = encode_utf16_like(filepath, 512)
    binary_field = create_binary_field(duration, len(filepath))
    binary_field_padded = binary_field + b'\x00' * (204 - len(binary_field))
    title_field = encode_utf16_like(title, 72)
    return filename_field + binary_field_padded + title_field

def create_playlist_file(
    file_entries: list[tuple[str, Optional[int], Optional[str]]],
    output_path: Optional[str] = None,
    directory: Optional[str] = None,
):
    header = b'MUSICARRAY SAVEFILE 01.00.0'
    records = b''
    for path, duration, title in file_entries:
        records += create_playlist_record(path, duration, title, directory)
    with open(output_path, 'wb') if output_path else sys.stdout.buffer as f:
        f.write(header)
        f.write(records)

def create_listinfo_entry(playlist_path: str) -> bytes:
    tag_field = struct.pack("<H", 3).ljust(8, b'\x00')
    path_field = encode_utf16_like(playlist_path, 512)
    length_field = struct.pack("<H", len(playlist_path)).ljust(12, b'\x00')
    return tag_field + path_field + length_field

if __name__ == "__main__":
    album_title = sys.argv[1]
    album_slug = album_title.lower().replace(" ", "_")
    album_abbr = "".join([word[0].lower() for word in album_title.split()])
    mount_path = sys.argv[2]
    drive_name = "E:\\"
    music_path = "Music files\\"
    listinfo_path = sys.argv[3]
    with open(f"{album_slug}/playlist.txt", "r") as text_playlist:
        create_playlist_file(
            [(
                f"{drive_name}{music_path}{album_abbr}\\{line.strip()}",
                None, None
            ) for line in text_playlist],
            f"{mount_path}/System/Mp3_res/{album_title}.lst",
            album_slug
        )
    with open(listinfo_path, "ab") as listinfo:
        listinfo.write(create_listinfo_entry(
            f"{drive_name}System\\Mp3_res\\{album_title}.lst"
        ))

