[Python] Can when changing song position qdbus6/bluez does not update position metadata

Discussão em 'Python' iniciado por Stack, Junho 2, 2025.

  1. Stack

    Stack Membro Participativo

    when skipping backwards or changing the playback position of a song qdbus6 or bluez is not refreshing

    if I rewind or scan in any direction I do not get a proper reading in the "Position" value instead it continues counting in milliseconds until it the song ends on the source device

    Browsable: true
    Device:
    Name: Music
    Playlist:
    Position: 45044
    Repeat: off
    Searchable: true
    Shuffle: alltracks
    Status: playing
    Subtype: None
    Track: Album: Devil (feat. Bubi) - Single
    Artist: Ironmouse, shirobeats & HalaCG
    Duration: 189677
    Genre: Pop
    Item:
    NumberOfTracks: 550
    Title: Devil (feat. Bubi)
    TrackNumber: 100
    Type: Audio


    I have tried manually issuing various control commands to see if there is something that can do it but I don't have the "Update" command so I am having trouble figuring out what is going on.

    this is what it does when I adjust the position on the playback device and it goes past the playback range instead of of resetting when adjusted.

    Browsable: true
    Device:
    Name: Music
    Playlist:
    Position: 89961
    Repeat: off
    Searchable: true
    Shuffle: alltracks
    Status: playing
    Subtype: None
    Track: Album: New Classics (2010-2016)
    Artist: dj-Nate
    Duration: 88163
    Genre: Dance
    Item:
    NumberOfTracks: 550
    Title: Dirty Thumpin' Saws
    TrackNumber: 107
    Type: Audio


    the code for the entire app I am having is here

    import curses
    import subprocess
    import argparse
    import os
    import time

    def run_qdbus6(args):
    try:
    return subprocess.check_output(["qdbus6", "--system"] + args, text=True)
    except Exception:
    return ""

    def parse_status(output: str) -> dict:
    info = {}
    for line in output.splitlines():
    if ':' in line:
    k, v = line.split(':', 1)
    info[k.strip()] = v.strip()
    return info

    def ms_to_mins_secs(ms):
    seconds = int(ms) // 1000
    mins = seconds // 60
    secs = seconds % 60
    return f"{mins}:{secs:02}"

    def check_device_connected(mac_addr):
    """Returns True if the device is connected, False otherwise."""
    dev_path = f"/org/bluez/hci0/dev_{mac_addr.replace(':', '_')}"
    try:
    output = subprocess.check_output(
    ["qdbus6", "--system", "org.bluez", dev_path, "org.freedesktop.DBus.Properties.Get", "org.bluez.Device1", "Connected"],
    text=True,
    stderr=subprocess.DEVNULL
    )
    return "true" in output.lower()
    except Exception:
    return False

    def figlet_centered(stdscr, text, color_pair=0):
    max_y, max_x = stdscr.getmaxyx()
    try:
    figlet_out = subprocess.check_output(
    ["figlet", "-f", "slant", "-l", "-w", str(max_x), text],
    text=True,
    stderr=subprocess.DEVNULL
    )
    except Exception:
    figlet_out = text
    lines = figlet_out.splitlines()
    y0 = max((max_y - len(lines)) // 2, 0)
    for i, line in enumerate(lines):
    x0 = max((max_x - len(line)) // 2, 0)
    try:
    stdscr.addstr(y0 + i, x0, line, curses.color_pair(color_pair))
    except curses.error:
    pass

    def draw_ui(stdscr, info, highlight_idx=None, button_boxes=None, color_pair=0):
    stdscr.clear()
    max_y, max_x = stdscr.getmaxyx()
    title = info.get("Title", "Unknown Title")
    artist = info.get("Artist", "Unknown Artist")
    album = info.get("Track: Album", info.get("Album", "Unknown Album"))
    status = info.get("Status", "paused").capitalize()
    position = int(info.get("Position", "0"))
    duration = int(info.get("Duration", "1"))
    elapsed = ms_to_mins_secs(position)
    remaining = ms_to_mins_secs(max(duration - position, 0))
    prog_pos = position // 1000
    prog_dur = max(duration // 1000, 1)

    stdscr.addstr(0, 0, "=" * max_x, curses.color_pair(color_pair))
    stdscr.addstr(1, 0, title.center(max_x), curses.color_pair(color_pair))
    stdscr.addstr(2, 0, artist.center(max_x), curses.color_pair(color_pair))
    stdscr.addstr(3, 0, album.center(max_x), curses.color_pair(color_pair))
    stdscr.addstr(4, 0, "=" * max_x, curses.color_pair(color_pair))
    stdscr.addstr(5, 0, f"Status: {status}", curses.color_pair(color_pair))

    bar_length = max(max_x - 23, 10)
    filled_length = int(bar_length * prog_pos // prog_dur) if prog_dur else 0
    bar = "[" + "=" * filled_length + ">" + " " * (bar_length - filled_length - 1) + "]"
    stdscr.addstr(7, 0, f"{elapsed.rjust(5)} {bar} -{remaining}", curses.color_pair(color_pair))

    labels = ["⏮", "⏯" if status.lower() != "playing" else "⏸", "⏭"]
    button_boxes.clear()
    btn_y = 9
    btn_h = 5
    btn_w = 13
    gap = 6
    total_btns = 3 * btn_w + 2 * gap
    start_x = (max_x - total_btns) // 2
    for i, label in enumerate(labels):
    x = start_x + i * (btn_w + gap)
    y = btn_y
    box = (y, x, y + btn_h, x + btn_w)
    button_boxes.append(box)
    for bx in range(x, x+btn_w):
    stdscr.addch(y, bx, curses.ACS_HLINE, curses.color_pair(color_pair))
    stdscr.addch(y+btn_h-1, bx, curses.ACS_HLINE, curses.color_pair(color_pair))
    for by in range(y, y+btn_h):
    stdscr.addch(by, x, curses.ACS_VLINE, curses.color_pair(color_pair))
    stdscr.addch(by, x+btn_w-1, curses.ACS_VLINE, curses.color_pair(color_pair))
    stdscr.addch(y, x, curses.ACS_ULCORNER, curses.color_pair(color_pair))
    stdscr.addch(y, x+btn_w-1, curses.ACS_URCORNER, curses.color_pair(color_pair))
    stdscr.addch(y+btn_h-1, x, curses.ACS_LLCORNER, curses.color_pair(color_pair))
    stdscr.addch(y+btn_h-1, x+btn_w-1, curses.ACS_LRCORNER, curses.color_pair(color_pair))
    for by in range(y+1, y+btn_h-1):
    stdscr.addstr(by, x+1, " " * (btn_w-2), curses.color_pair(color_pair) | (curses.A_REVERSE if highlight_idx == i else curses.A_NORMAL))
    stdscr.addstr(y + btn_h//2, x + (btn_w-len(label))//2, label, curses.A_BOLD | curses.color_pair(color_pair) | (curses.A_REVERSE if highlight_idx == i else 0))
    stdscr.addstr(btn_y+btn_h+1, 0, "Controls: [p] Play/Pause [n] Next Previous [q] Quit".center(max_x), curses.color_pair(color_pair))
    stdscr.addstr(btn_y+btn_h+2, 0, "=" * max_x, curses.color_pair(color_pair))
    stdscr.refresh()

    def which_button(mx, my, button_boxes):
    for idx, (y1, x1, y2, x2) in enumerate(button_boxes):
    if y1 <= my < y2 and x1 <= mx < x2:
    return idx
    return None

    def mac_to_bluez(mac):
    return mac.replace(":", "_").upper()

    def bluez_to_mac(bluez_mac):
    # Accepts MAC with underscores, returns colon-separated MAC
    return bluez_mac.replace("_", ":").upper()

    def device_not_found_screen(stdscr, bluez_mac, color_pair, autoretry_interval=1.0):
    shown_mac = bluez_to_mac(bluez_mac)
    retry_counter = 0
    while True:
    stdscr.clear()
    figlet_centered(stdscr, shown_mac, color_pair=color_pair)
    msg = f"Device not connected. Waiting for device... (auto-retry every {autoretry_interval:.1f}s, press [q] to quit)".center(stdscr.getmaxyx()[1])
    stdscr.addstr(stdscr.getmaxyx()[0]-2, 0, msg, curses.color_pair(color_pair))
    stdscr.refresh()
    wait_start = time.time()
    # Wait for either timeout or 'q'
    while True:
    key = stdscr.getch()
    if key in [ord('q'), ord('Q')]:
    return False
    elif key == ord('r'): # legacy, not needed, but kept for any custom testing
    break
    # After interval, break from inner loop to retry connection
    if time.time() - wait_start > autoretry_interval:
    break
    time.sleep(0.05)
    # After waiting, check connection
    mac_addr = shown_mac
    if check_device_connected(shown_mac):
    return True

    def main(stdscr, mac_addr, color_pair, autorefresh_interval=1.0):
    curses.curs_set(0)
    curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
    stdscr.nodelay(True)
    bluez_mac = mac_to_bluez(mac_addr)
    PLAYER_PATH = f"/org/bluez/hci0/dev_{bluez_mac}/player0"

    button_boxes = []

    def refresh_info():
    status_out = run_qdbus6([
    "org.bluez",
    PLAYER_PATH,
    "org.freedesktop.DBus.Properties.GetAll",
    "org.bluez.MediaPlayer1"
    ])
    return parse_status(status_out)

    # Device connect/retry loop (initial)
    while not check_device_connected(mac_addr):
    found = device_not_found_screen(stdscr, bluez_mac, color_pair, autoretry_interval=autorefresh_interval)
    if not found:
    return

    info = refresh_info()
    draw_ui(stdscr, info, None, button_boxes, color_pair=color_pair)
    last_refresh = time.time()
    while True:
    try:
    key = stdscr.getch()
    now = time.time()

    # At each loop, check if device is still connected
    if not check_device_connected(mac_addr):
    found = device_not_found_screen(stdscr, bluez_mac, color_pair, autoretry_interval=autorefresh_interval)
    if not found:
    return
    info = refresh_info()
    draw_ui(stdscr, info, None, button_boxes, color_pair=color_pair)
    last_refresh = time.time()
    continue

    # Autorefresh every autorefresh_interval seconds
    if now - last_refresh > autorefresh_interval:
    info = refresh_info()
    draw_ui(stdscr, info, None, button_boxes, color_pair=color_pair)
    last_refresh = now

    if key == ord('q'):
    break
    elif key in [ord('p'), ord(' ')]:
    status = info.get("Status", "paused").lower()
    if status == "playing":
    run_qdbus6([
    "org.bluez",
    PLAYER_PATH,
    "org.bluez.MediaPlayer1.Pause"
    ])
    else:
    run_qdbus6([
    "org.bluez",
    PLAYER_PATH,
    "org.bluez.MediaPlayer1.Play"
    ])
    info = refresh_info()
    draw_ui(stdscr, info, 1, button_boxes, color_pair=color_pair)
    last_refresh = time.time()
    elif key in [ord('n'), curses.KEY_RIGHT]:
    run_qdbus6([
    "org.bluez",
    PLAYER_PATH,
    "org.bluez.MediaPlayer1.Next"
    ])
    info = refresh_info()
    draw_ui(stdscr, info, 2, button_boxes, color_pair=color_pair)
    last_refresh = time.time()
    elif key in [ord('b'), curses.KEY_LEFT]:
    run_qdbus6([
    "org.bluez",
    PLAYER_PATH,
    "org.bluez.MediaPlayer1.Previous"
    ])
    info = refresh_info()
    draw_ui(stdscr, info, 0, button_boxes, color_pair=color_pair)
    last_refresh = time.time()
    elif key == curses.KEY_MOUSE:
    _, mx, my, _, bstate = curses.getmouse()
    btn_idx = which_button(mx, my, button_boxes)
    if btn_idx == 0: # prev
    run_qdbus6([
    "org.bluez",
    PLAYER_PATH,
    "org.bluez.MediaPlayer1.Previous"
    ])
    elif btn_idx == 1: # play/pause
    status = info.get("Status", "paused").lower()
    if status == "playing":
    run_qdbus6([
    "org.bluez",
    PLAYER_PATH,
    "org.bluez.MediaPlayer1.Pause"
    ])
    else:
    run_qdbus6([
    "org.bluez",
    PLAYER_PATH,
    "org.bluez.MediaPlayer1.Play"
    ])
    elif btn_idx == 2: # next
    run_qdbus6([
    "org.bluez",
    PLAYER_PATH,
    "org.bluez.MediaPlayer1.Next"
    ])
    info = refresh_info()
    draw_ui(stdscr, info, btn_idx, button_boxes, color_pair=color_pair)
    last_refresh = time.time()
    elif key == -1:
    pass
    except KeyboardInterrupt:
    break

    def parse_args():
    parser = argparse.ArgumentParser(description="Bluetooth Music Player UI with Mouse and Keyboard Control")
    parser.add_argument('-D', '--device', type=str, required=True, help="Bluetooth MAC address of the device")
    parser.add_argument('--color', type=str, default="white", help="Color theme (default: white)")
    parser.add_argument('--autorefresh', type=float, default=1.0, help="Autorefresh interval in seconds (default: 1.0)")
    return parser.parse_args()

    def color_theme(theme):
    colors = dict(
    black=curses.COLOR_BLACK, red=curses.COLOR_RED, green=curses.COLOR_GREEN, yellow=curses.COLOR_YELLOW,
    blue=curses.COLOR_BLUE, magenta=curses.COLOR_MAGENTA, cyan=curses.COLOR_CYAN, white=curses.COLOR_WHITE
    )
    base = colors.get(theme.lower(), curses.COLOR_WHITE)
    curses.init_pair(1, base, curses.COLOR_BLACK)
    return 1

    def run():
    args = parse_args()
    mac_addr = args.device
    def wrapped(stdscr):
    curses.start_color()
    color_pair = color_theme(args.color)
    main(stdscr, mac_addr, color_pair, autorefresh_interval=args.autorefresh)
    curses.wrapper(wrapped)

    if __name__ == "__main__":
    run()


    I noticed there were various another questions here about this issue but I did not see an answer on any of them.

    Continue reading...

Compartilhe esta Página