Full Code of elibroftw/music-caster for AI

master 203400531a45 cached
98 files
875.3 KB
255.2k tokens
455 symbols
1 requests
Download .txt
Showing preview only (920K chars total). Download the full file or copy to clipboard to get everything.
Repository: elibroftw/music-caster
Branch: master
Commit: 203400531a45
Files: 98
Total size: 875.3 KB

Directory structure:
gitextract_zrp3by6s/

├── .dockerignore
├── .github/
│   └── workflows/
│       ├── build.yml
│       └── winget.yml
├── .gitignore
├── CHANGELOG.txt
├── Dockerfile
├── LICENSE
├── README.md
├── build.cmd
├── build.py
├── build_files/
│   ├── TkinterDnD2/
│   │   ├── TkinterDnD.py
│   │   └── __init__.py
│   ├── Updater.cs.txt
│   ├── Updater.exe.MANIFEST
│   ├── daemon.spec
│   ├── flatpak-pip-generator.py
│   ├── mc_version_info.txt
│   ├── mcu_version_info.txt
│   ├── onedir.spec
│   ├── portable.spec
│   ├── pyaudio-0.2.14-cp314-cp314-win_amd64.whl
│   ├── pyinstaller-6.16.0-py3-none-any.whl
│   ├── setup_script.iss
│   ├── tkdnd2.9.2/
│   │   ├── pkgIndex.tcl
│   │   ├── tkdnd.tcl
│   │   ├── tkdnd2.9.2.lib
│   │   ├── tkdnd_compat.tcl
│   │   ├── tkdnd_generic.tcl
│   │   ├── tkdnd_macosx.tcl
│   │   ├── tkdnd_unix.tcl
│   │   ├── tkdnd_utils.tcl
│   │   └── tkdnd_windows.tcl
│   └── updater.spec
├── conftest.py
├── linux_install.py
├── linux_install.sh
├── music_caster.desktop
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── resources/
│   ├── Music Caster Icon.psd
│   ├── assets.psd
│   ├── favicons/
│   │   ├── browserconfig.xml
│   │   └── site.webmanifest
│   ├── gude-2023-11-11.log
│   └── icons/
│       └── icon.icns
├── scripts/
│   ├── arch-install.sh
│   ├── debian-install.sh
│   ├── fedora-install.sh
│   ├── pre-req.sh
│   └── suse-install.sh
└── src/
    ├── audio_player.py
    ├── b64_images.py
    ├── ca.elijahlopez.MusicCaster.yml
    ├── experiments.py
    ├── go.mod
    ├── go.sum
    ├── gui/
    │   ├── __init__.py
    │   ├── components.py
    │   └── views.py
    ├── knownpaths.py
    ├── languages/
    │   ├── da.txt
    │   ├── de.txt
    │   ├── en.txt
    │   ├── es.txt
    │   ├── fr.txt
    │   ├── it.txt
    │   ├── nl.txt
    │   ├── pt-br.txt
    │   ├── ru.txt
    │   ├── sk.txt
    │   └── uk.txt
    ├── meta.py
    ├── modules/
    │   ├── db.py
    │   ├── error_reporting.py
    │   ├── iph1papi.py
    │   ├── playing_status.py
    │   ├── resolution_switcher.py
    │   ├── url_metadata.py
    │   └── win32_media_controls.py
    ├── music_caster.bat
    ├── music_caster.py
    ├── pyoxidizer.bzl
    ├── shared.py
    ├── static/
    │   └── style.css
    ├── sys_tray.py
    ├── templates/
    │   └── index.html
    ├── test_cases/
    │   └── ipconfig.py
    ├── test_harness.py
    ├── theme/
    │   ├── LICENSE
    │   ├── dark.tcl
    │   ├── light.tcl
    │   └── sun-valley.tcl
    ├── updater.go
    ├── utils.py
    ├── vlc_lib/
    │   ├── libvlc.so.5.6.0
    │   └── libvlccore.so.9.0.0
    └── webview_demo.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
.ruff_cache/
.pytest_cache/
git/
github/
.idea/
.vscode/
__pycache__/
src/venv/
src/.venv/
build/
_build/
dist/
images/
test_files/
venv/
.venv/
src/.flatpak-builder
.env
settings.json
settings.json.bak
test.*
demo.py
error.log
music_caster.log*
resources/assets.png
.vs/
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
*.userprefs
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
test.txt
music_caster.log
music_caster.log1
resources/assets.png
settings.json.bak
*.syso
*.pid
music_caster.lock
music_caster.db
src/phantomjs/


================================================
FILE: .github/workflows/build.yml
================================================
name: App Builder
on:
  push:
    branches: master
  workflow_dispatch:
concurrency:
  cancel-in-progress: true
  # we want to group based on the push type not the ref
  group: ${{ github.ref_type }}

env:
  ARTIFACT_BASE_NAME: 'music_caster_artifacts' # + '_${runsOn}'
  github: ${{ secrets.GITHUB_TOKEN }}

jobs:
  build:
    strategy:
      matrix:
        platform: [windows-latest]
    runs-on: ${{matrix.platform}}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: 3.14
          cache: pip
      - uses: actions/setup-go@v5

      - name: Install Inno Setup
        run: choco install innosetup --yes

      - name: Install system deps
        run: python build.py --deps

      - name: Create Python venv
        run: |
          python -m venv .venv

      - name: Install venv dependencies
        run: |
          .venv\Scripts\Activate.ps1
          python build.py --deps

      - name: Build
        run: |
          .venv\Scripts\Activate.ps1
          python build.py --ci --no-deps --no-tests --no-install

      - name: Test build
        id: test
        run: |
          .venv\Scripts\Activate.ps1
          python build.py --ci --no-deps --no-build --no-install

      - name: Save Failed Build
        if: failure() && steps.test.outcome == 'failure'
        uses: actions/upload-artifact@v4
        with:
          name: failed-build
          path: |
            dist/Music Caster Setup.exe

      # upload the build continuously to avoid duplicate build on a tag
      # script will exit with zero if release for VERSION exists since --ci is used
      - name: Upload build
        if: ${{ github.event_name == 'push' && github.ref_name == 'master' }}
        run: |
          .venv\Scripts\Activate.ps1
          python build.py -u --ci --no-deps --no-build --no-tests --no-install

  # TODO: if/when MacOS is supported?
  # release:
  #   needs: build
  #   runs-on: ubuntu-latest


================================================
FILE: .github/workflows/winget.yml
================================================
name: Publish to WinGet
on:
  release:
    types: [released]
jobs:
  publish:
    runs-on: windows-latest # action can only be run on windows
    steps:
      - uses: vedantmgoyal2009/winget-releaser@v2
        with:
          identifier: ElijahLopez.MusicCaster
          token: ${{ secrets.WINGET_TOKEN }}


================================================
FILE: .gitignore
================================================
.idea/
.vscode/
__pycache__/
src/venv/
src/.venv/
build/
_build/
dist/
images/
test_files/
venv/
.venv/
src/.flatpak-builder
.env
settings.json
settings.json.bak
test.*
demo.py
error.log
music_caster.log*
resources/assets.png
.vs/
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
*.userprefs
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
test.txt
music_caster.log
music_caster.log1
resources/assets.png
settings.json.bak
*.syso
*.pid
music_caster.lock
music_caster.db
src/phantomjs/
src/music_caster.db-journal
src/thumb.jpg
app/node_modules/
app/src-tauri/target/
stats.html


================================================
FILE: CHANGELOG.txt
================================================
Music Caster Changelog

5.25.2
- [Fix] URL processing

5.25.1
- [Fix] Settings save/load ?
- [Fix] Typing

5.25.0
- [Fix] Forwards compatibility for v6
- [Fix] Maintenance (Python 3.14)

5.24.0
- [Feat] Performance when indexing many URLs
- [Fix] Maintenance (Python 3.14)

5.23.8
- [Fix] URL processing

5.23.7
- [Fix] Death loop

5.23.6
- [Fix] URL processing

5.23.5
- [Fix] URL processing

5.23.4
- [Fix] URL processing

5.23.3
- [Fix] URL processing

5.23.2
- [Fix] Reopen GUI on Restart

5.23.1
- [Fix] URL processing

5.23.0
- [UI] Added Slovak translation

5.22.22
- [Fix] Radio-like streams

5.22.21
- [Fix] Volume syncing
- [Fix] Improved reconnecting upon abrupt disconnect

5.22.20
- [Fix] Album art for cast devices

5.22.19
- Upgrade dependencies

5.22.18
- [Fix] Album art optimization for cast devices

5.22.17
- [Fix] Translation for restarting

5.22.16
- [Fix] Album art optimization for cast devices

5.22.15
- [Fix] Album art optimization for cast devices

5.22.14
- [Fix] Track position syncing with cast

5.22.13
- [Misc] Add more logging

5.22.12
- [Fix] Do not stop if cast app id is None

5.22.11
- [Misc] Add more logging

5.22.10
- [Fix] Track numbers from MP4/M4A files

5.22.9
- [Fix] Save file corruption on system crash

5.22.8
- [Fix] Save file corruption on system crash
- [Fix] URL processing

5.22.7
- [Fix] URL processing

5.22.6
- [Fix] URL processing

5.22.5
- [Fix] DE translation

5.22.4
- [Fix] Playing from shell (REST API)

5.22.3
- [Feat] Support "System Audio" in REST API

5.22.2
- [Feat] Support "System Audio" in CLI

5.22.1
- [Fix] URL processing

5.22.0
- [UI] Added Português (Brazil) translation
- [Fix] URL processing

5.21.3
- [Fix] Auto update
- [Fix] Button font legibility2

5.21.2
- [Fix] Notification app id
- [Fix] Security update

5.21.1
- [Fix] Open web gui

5.21.0
- [UI] Rounded rectangle buttons
- [UI] Improve spacing and alignments

5.20.2
- [Fix] Hide install update button

5.20.1
- [Fix] Getting local IP address for casting
- [Fix] Update notifier

5.20.0
- [Fix] Getting local IP address for casting

5.19.13
- [Fix] Code cleanup
- [Fix] Sort Library by artist by default (don't know why I took so many years to do this)

5.19.12
- [Fix] Local playback scrubbing

5.19.11
- [Experiment] Better connection?

5.19.10
- [Fix] Unexpected crash - chromecast related

5.19.9
- [Fix] Playing offline if URL in queue

5.19.8
- [Fix] Unexpected crash when chromecast stops working

5.19.7
- [Fix] Unexpected metadata while playing (dirty patch)

5.19.6
- [Fix] Experimental theme setting
- [Fix] Saving metadata without album art

5.19.5
- [Fix] Handling unresponsive cast device

5.19.4
- [Fix] Crash when playing

5.19.3
- [Fix] Cast syncing

5.19.2
- [Fix] Update from GUI

5.19.1
- [Fix] Windows-specific issues

5.19.0
- [Feat] Update from GUI

5.18.9
- [Fix] Locally played track scrubbing

5.18.8
- [Fix] Improve track scrubbing performance

5.18.7
- [Fix] logging

5.18.6
- [Fix] GUI freezing from seeking
- [Fix] GUI improve set volume performance

5.18.5
- [Fix] GUI freezing from seeking

5.18.4
- [Fix] Increase wait timeout

5.18.3
- [Fix] Handle experimental feature error

5.18.2
- [Fix] Changing cast device

5.18.1
- [Fix] Security update
- [Fix] Play AIFF from explorer

5.18.0
- [Feat] Local support for AIFF?

5.17.7
- [GUI] Timer tab translation

5.17.6
- [Fix] Better exception handling when pausing Chromecast

5.17.5
- [Fix] Setting position of Chromecast from GUI again

5.17.4
- [Fix] Volume Chromecast

5.17.3
- [Fix] Upgrade pychromecast

5.17.2
- [Fix] Pausing Chromecast

5.17.1
- [Fix] Setting position of Chromecast from GUI

5.17.0
- [Feat] Migrated error capturing endpoint from pipedream to under mine (lenerva)

5.16.8
- [Fix] Chromecast status

5.16.7
- [Fix] Chromecast status

5.16.6
- [Fix] Chromecast status callback

5.16.5
- [Fix] Chromecast status callback

5.16.4
- [Fix] pychromecast v14 support

5.16.3
- [Fix] use GitHub downloads instead of instances

5.16.2
- [Fix] Metadata time modified error
- [Fix] Clear queue

5.16.1
- [Fix] Remove old startup shortcuts + background thread

5.16.0
- [Feat] Linux support
- [Fix] Remove old startup shortcuts

5.15.0
- [Fix] Security update

5.14.0
- [Dev] New build process; might break app

5.13.43
- [Fix] Stop chromecast after 5 minutes of pausing

5.13.42
- [Fix] Install size
- [Fix] False positive not playing on chromecast

5.13.41
- [Fix] Use latest Pillow

5.13.40
- [Fix] Revert PyInstaller upgrade causing anti-virus detection

5.13.39
- [Fix] Exit program

5.13.38
- [Fix] Added error handling for Metadata editor artwork

5.13.37
- [Fix] Select metadata art from other audio files

5.13.36
- [Fix] Linux local audio player
- [Fix] Default music folder

5.13.35
- [Fix] register handlers

5.13.34
- [Fix] Start on login

5.13.33
- [Fix] Playlist delete

5.13.32
- [Fix] Fetching local IPV4

5.13.31
- [UI] Danish supported
- [Fix] Playlist tab

5.13.30
- [Feat] Playlist length
- [UI] ListBox controls alignment

5.13.29
- [Fix] Experimental UI tooltip text now legible

5.13.28
- [UI] Add url with <Enter>
- [UI] UI informs when playlist was saved by user

5.13.27
- [Fix] URL formatting

5.13.26
- [Fix] web UI and API

5.13.25
- [Fix] web UI and API

5.13.24
- [Fix] get_ipv4 fallback bug

5.13.23
- [Fix] Next up bug
- [Fix] Don't cache spotify playlist
- [Fix] Metadata bug
- [Fix] Locate track bug

5.13.22
- [UI] Tooltip for all settings

5.13.21
- [UI] Added French translation

5.13.20
- [UI] Added Russian translation
- [Fix] Sort order when playing folder with missing album metadata
- [Fix] Ukrainian translation

5.13.19
- [UI] Show artist as unknown in queue

5.13.18
- [UI] Added Ukrainian translation

5.13.17
- Remove old code

5.13.16
- Re-read metadata if file was modified

5.13.15
- Upgrade dependencies

5.13.14
- [Fix] set_pos crash when not connected

5.13.13
- [Fix] Shift + P for previous track

5.13.12
- [Fix] Ignore copyright errors
- [Fix] create_shortcut_windows

5.13.11
- [Fix] Metadata select artwork allows JPG/JPEG images

5.13.10
- Upgrade dependencies

5.13.9
- Rollback dependencies

5.13.8
- Upgrade dependencies

5.13.7
- Upgrade dependencies

5.13.6
- [Fix] Opening GUI with timer

5.13.5
- [Fix] PhantomJS auto install

5.13.4
- [Fix] SSL links

5.13.3
- [Fix] Playing YouTube playlists

5.13.2
- [Fix] Local address fetcher

5.13.1
- [Fix] Local address fetcher

5.13.0
- [Feat] Works with VPN

5.12.12
- [Fix] ytdlp

5.12.11
- [Fix] ytdlp

5.12.10
- [Fix] ytdlp

5.12.9
- [Fix] Open PhantomJS download page

5.12.8
- [Fix] Weird metadata

5.12.7
- [Fix] Invalid URL crash

5.12.6
- [Fix] Resolution switcher
- Upgrade dependencies

5.12.5
- [Fix] Ctrl + C for queue

5.12.4
- [Fix] Deezer, Spotify search

5.12.3
- [Fix] Exit app crash

5.12.2
- Upgrade dependencies

5.12.1
- [Fix] Queue action accuracy
- [Note] New REST API

5.12.0
- [Feat] Edit metadata of a track in the queue
- [Feat] Save selected items in queue to playlist
- [Feat] Alternative UI (Settings > Misc > Experimental features)
- [Fix] Handle user prematurely removing bad uri

5.11.0
- [Feat] Copy playlist URIs
- [Fix] upgrade PyInstaller

5.10.4
- [Fix] locate uri

5.10.3
- [Fix] Volume sync with chromecast

5.10.2
- [Fix] Exit program after sleeping

5.10.1
- [Fix] Relative audio files in Portable mode

5.10.0
- [Feat] Copy URIs (text only)
- [Fix] Move track up
- [Fix] Discord presence blocking

5.9.13
- [Fix] Play from stopped

5.9.12
- [Fix] Resolution switcher

5.9.11
- [Fix] Replaying queue from GUI

5.9.10
- [Fix] Non existent files

5.9.9
- [Fix] Covert art playlist videos

5.9.8
- [UX] YouTube source URL

5.9.7
- [Fix] Covert art

5.9.6
- [Fix] Long lived GUI bug

5.9.5
- [Optimization] YouTube playlists

5.9.4
- [Fix] Volume

5.9.3
- [Fix] Cast volume

5.9.2
- [Fix] No files selected
- [Fix] Playing URLs

5.9.1
- [Fix] Audio playback

5.9.0
- [Feat] YouTube livestreams

5.8.10
- [Fix] Improve API

5.8.9
- [Fix] Cast errors

5.8.8
- [Fix] Deezer URLs

5.8.7
- [Fix] Resolution setter

5.8.6
- [Fix] Deezer URLs

5.8.5
- [Fix] Update checker

5.8.4
- [Fix] Deezer URLs

5.8.3
- [Fix] URL metadata
- [Fix] Crashing when Cast NotConnected

5.8.2
- [Fix] Memory leaks

5.8.1
- [Fix] Also dynamically update refresh rate

5.8.0
- [Feat] Battery Resolution Switcher
- [Fix] Upgrade dependencies

5.7.9
- [Fix] Account for edge case

5.7.8
- [Fix] URL track number

5.7.7
- [Fix] Radio urls

5.7.6
- [Fix] Radio urls

5.7.5
- [Fix] FLAC album art

5.7.4
- [Fix] Cancel timer
- [Feat] Window only keyboard shortcuts

5.7.3
- [Fix] MP4/M4A track number

5.7.2
- [Fix] Album sorting
- [Fix] Files with no track number

5.7.1
- [Fix] German translation

5.7.0
- [UI] Added German / Deutsch translation

5.6.14
- [Fix] Improve GUI error handling

5.6.13
- [Fix] Improved sorting

5.6.12
- [Fix] Improved sorting for albums

5.6.11
- [Fix] Window position edge case

5.6.10
- [Fix] Cast not connected edge case

5.6.9
- [Fix] Folder select edge case

5.6.8
- [Fix] Locate track

5.6.7
- [Fix] Drag and drop files

5.6.6
- [Fix] Initial dir for dialog

5.6.5
- [Fix] TclError

5.6.4
- [Fix] URLs

5.6.3
- [Fix] Audio only URLs

5.6.2
- [Fix] Website integration

5.6.1
- [Fix] ujson security update

5.6.0
- [Feat] Save queue also saves position

5.5.7
- [Fix] YouTube URL Metadata

5.5.6
- [Fix] Web player UX

5.5.5
- [Fix] Open settings file

5.5.4
- [Fix] YouTube comment parsing
- [Fix] Queue multiple folders

5.5.3
- [Feat] &alb format

5.5.2
- [Fix] URL support

5.5.1
- [Fix] Volume GUI shortcut

5.5.0
- [Feat] Support more URLs

5.4.11
- [Fix] Volume

5.4.10
- [Fix] Volume

5.4.9
- [Fix] Web GUI settings
- [Fix] Background errors

5.4.8
- [Fix] VLC Race Condition

5.4.7
- [Fix] Better error handling

5.4.6
- [Fix] Reading Metadata
- [Fix] Spotify Playlists

5.4.5
- [Fix] Opus track number

5.4.4
- [Fix] Casting race conditions

5.4.3
- [Fix] Playback after startup race condition

5.4.2
- [Fix] Playlist Listbox

5.4.1
- [Fix] GUI

5.4.0
- [UI] Add device selection
- [UI] Simplified file/folder actions
- [UI] Fix Listbox height when artwork is off

5.3.1
- [UI] Update Web UI settings modal

5.3.0
- [Feat] Exit app on GUI close

5.2.5
- [Fix] Viewing changelog in python or portable mode

5.2.4
- [Fix] Race conditions

5.2.3
- [Fix] Cast pausing after playing

5.2.2
- [Fix] Unexpected behaviour after turning off cast

5.2.1
- [Fix] PyInstaller

5.2.0
- [UI] New Web UI layout
- [OS] Better Linux support

5.1.24
- [Fix] Single instance

5.1.23
- [Fix] Spotify playlists local files

5.1.22
- [Fix] Open settings.json

5.1.21
- [Fix] command line arguments

5.1.20
- [Feat] --device=<device> option added to specify the device to use
- [Feat] Ctrl + Shift + Q in GUI to exit program

5.1.19
- [Fix] URL casting

5.1.18
- [Fix] Youtube Playlist Web

5.1.17
- [Fix] Portable

5.1.16
- [Fix] Corner cases

5.1.15
- [Fix] URL Metadata

5.1.14
- [Fix] Encoding

5.1.13
- [Fix] Metadata

5.1.12
- [Fix] Startup

5.1.11
- [Fix] GUI

5.1.10
- [Fix] URL Metadata

5.1.9
- [Fix] URL Metadata

5.1.8
- [Fix] URL Metadata

5.1.7
- [Fix] URL Metadata

5.1.6
- [Fix] start_playing

5.1.5
- [Fix] Spelling

5.1.4
- [Fix] Chromecast connection

5.1.3
- [Fix] GUI Listbox

5.1.2
- [Optimization] Faster startup
- [Fix] Queue from explorer
- [Fix] Prev track
- [Fix] Cast updates

5.1.1
- [Fix] Override track format
- [Fix] URL Metadata

5.1.0
- [UI] Override track format
- [Fix] Dialog blocking

5.0.13
- [Fix] Long track names

5.0.12
- [Fix] M4A files

5.0.11
- [Fix] Setting png as cover art

5.0.10
- [Fix] Performance

5.0.9
- [Fix] TCLError Mini-mode

5.0.8
- [Fix] Temp TCLError fix

5.0.7
- [Fix] Prev track with next queue

5.0.6
- [Fix] Chromecast
- [Fix] URL metadata

5.0.5
- [Fix] Dependencies

5.0.4
- [Fix] Catch errors

5.0.3
- [Fix] Smart queue
- [Fix] Logging
- [Fix] Tray device names

5.0.2
- [Fix] TKdnd error

5.0.1
- [Fix] Better end of list behaviour

5.0.0
- [New] Using Python 3.10 64-bit

4.90.157
- [Fix] Invalid audio file error

4.90.156
- [Fix] Loading settings

4.90.155
- [Optimization] Mini-mode

4.90.154
- [Fix] Error handling

4.90.153
- [Fix] Chromecast connection issues

4.90.152
- [Fix] AAC, MP4, OggVorbis

4.90.151
- [Fix] Explorer play/playnext/queue behaviour

4.90.150
- [Fix] Opus file cover art
- [Fix] Keyboard shortcuts

4.90.149
- [Fix] Spotify links

4.90.148
- [Fix] Keyboard
- [Fix] Chromecast errors

4.90.147
- [Fix] URL

4.90.146
- [Fix] URL and Chromecast errors

4.90.145
- [Fix] Next/Previous track from queue

4.90.144
- [Fix] Handle chromecast disconnect

4.90.143
- [Fix] Previous track

4.90.142
- [Fix] Web GUI

4.90.141
- [Fix] Dutch

4.90.140
- [Fix] Change Chromecast

4.90.139
- [Fix] IPv6

4.90.138
- [Fix] Import playlist

4.90.137
- [Fix] Timer
- [Fix] Export playlist
- [UI] Added Dutch translation

4.90.136
- [Fix] URL support

4.90.135
- [Fix] get ipv4

4.90.134
- [Feat] Mobile playback
- [Fix] URL local muted playback

4.90.133
- [Fix] GUI activation

4.90.132
- [Fix] URL support

4.90.131
- [Fix] Metadata Tab
- [Fix] Discord Presence

4.90.130
- [Fix] GUI activation

4.90.129
- [Fix] APIs

4.90.128
- [Fix] YouTube and GUI focus

4.90.127
- [Fix] ComboBox button color

4.90.126
- [UX] Support music.youtube.com
- [UI] Added link to GitHub
- [UX] Flash border of mini-mode

4.90.125
- [Fix] Improved pause and resume logging

4.90.124
- [Fix] Log file location

4.90.123
- [Fix] Tray not closing

4.90.122
- [Fix] Move track down

4.90.121
- [Fix] Focus on first window creation

4.90.120
- [Fix] json serialization

4.90.119
- [Fix] Activate instance using IPv6

4.90.118
- [Fix] Race condition if previous_device cannot be found
- [Fix] Allow IPv6 connections

4.90.117
- [Fix] Playing from command line arguments
- [Fix] Chromecast errors

4.90.116
- [Fix] Cleaner dual instance exit
- [Fix] Improved /state/ and

4.90.115
- [Fix] Cleaner exit

4.90.114
- [Fix] Revert minimizing when closing

4.90.113
- [Fix] Chromecast errors
- [Fix] Discord rich presence
- [Optimization] Improve startup time by reducing top level imports
- [Optimization] Using ujson to load and save settings faster
- [Optimization] Auto-update does not block usage

4.90.112
- [Fix] Don't send known errors

4.90.111
- [Fix] URL parsing if URL results in 404

4.90.110
- [Fix] Maybe fix cast connection issues

4.90.109
- [Fix] Tray closing on non-fatal error

4.90.108
- [Fix] Auto-update on system startup

4.90.107
- [Fix] Disalbed Discord RPC For Sure

4.90.106
- [Fix] More error catching

4.90.105
- [Fix] Tray theme

4.90.104
- [Fix] Already connected errors

4.90.103
- [Fix] Portable

4.90.102
- [Fix] Updated pychromecast and zeroconf

4.90.101
- [Fix] Disabled Discord RPC

4.90.100
- [Fix] Files web UI

4.90.99
- [Fix] Fix mini-mode bug

4.90.98
- [Fix] Fix reading explicit/rating tag

4.90.97
- [Fix] Add more DEBUG features

4.90.96
- [Fix] Better YouTube playback

4.90.95
- [Fix] Handle bad metadata input
- [Fix] Update Flask semantics

4.90.94
- [Fix] Improved --help output

4.90.93
- [Fix] Web UI upload button

4.90.92
- [Fix] Web UI modal CSS

4.90.91
- [Fix] Web UI Mobile CSS
- [Feat] Web UI Dark Theme

4.90.90
- [Fix] Folder action when using a different language

4.90.89
- [Feat] New /devices/ API
- [Fix] Translation for "locate track"

4.90.88
- [Fix] Removed polling
- [Fix] Fixed anti-virus detection?

4.90.87
- [Fix] Polling (Hibernation bug)

4.90.86
- [Fix] Polling

4.90.85
- [Fix] Strip URL's

4.90.84
- [Fix] Proxies

4.90.83
- [Fix] Polling

4.90.82
- [Fix] Polling

4.90.81
- [Fix] Window focus
- [Feat] Specify delay (beta)

4.90.80
- [Fix] Auto-update restart minimized

4.90.79
- [Feat] Open window when run

4.90.78
- [Fix] Update dependencies

4.90.77
- [Fix] Fix YouTube link expiry

4.90.76
- [Fix] Better tray exit behaviour

4.90.75
- [Fix] Timer 24hr support

4.90.74
- [Fix] YT Comment Parser

4.90.73
- [Fix] Timer settings Web GUI

4.90.72
- [Fix] Default volume now 50%

4.90.71
- [Fix] YT Comment Parser

4.90.70
- [Fix] Added more logging

4.90.69
- [Fix] Web GUI

4.90.68
- [Fix] Metadata

4.90.67
- [Fix] Mini-mode drag

4.90.66
- [Fix] YouTube parsing

4.90.65
- [Fix] Scanning

4.90.64
- [Fix] Tray devices

4.90.63
- [Fix] Combo boxes

4.90.62
- [Fix] Library selection colors

4.90.61
- [Fix] URL

4.90.60
- [Fix] GUI

4.90.59
- [Feat] Upload files to PC

4.90.58
- [Fix] Buffering

4.90.57
- [Fix] Buffering

4.90.56
- [Fix] Icon

4.90.55
- [Fix] GUI

4.90.54
- [Fix] M3U files

4.90.53
- [Fix] Timer API

4.90.52
- [Fix] GUI

4.90.51
- [Fix] GUI

4.90.50
- [Fix] MP4/M4A/AAC

4.90.49
- [Fix] Export playlist

4.90.48
- [Fix] Web GUI
- [Fix] Playlist tab

4.90.47
- [Fix] MP4/M4A/AAC

4.90.46
- [Fix] MP4/M4A/AAC

4.90.45
- [Fix] Playlist
- [Fix] MP4/M4A/AAC

4.90.44
- [Fix] Flac files
- [Fix] Better proxy support

4.90.43
- [Fix] Favicon

4.90.42
- [Fix] Favicon

4.90.41
- [Fix] Settings tooltip

4.90.40
- [Fix] Metadata tab

4.90.39
- [Fix] Mini-mode

4.90.38
- [Fix] Notifications and upgrade Python

4.90.37
- [Fix] Mini-mode

4.90.36
- [Optimization] GUI
- [Optimization] Scrubbing

4.90.35
- [Fix] Auto next track

4.90.34
- [Optimization] Smaller executable

4.90.33
- [Fix] Expired URLs

4.90.32
- [Fix] Cut/Copy url inputs

4.90.31
- [Fix] GUI
- [Fix] Stop

4.90.30
- [Optimization] Memory

4.90.29
- [Fix] Metadata setting

4.90.28
- [Fix] Switching devices
- [Fix] Seeking

4.90.27
- [Fix] Web GUI

4.90.26
- [Fix] Queue all

4.90.25
- [Feat] Add Mini-mode shortcut
- [Fix] Album art setting

4.90.24
- [Fix] Spotify

4.90.23
- [Fix] Italian translation
- [Fix] DND URL support

4.90.22
- [Fix] Timer button

4.90.21
- [Fix] Cookie parsing

4.90.20
- [Fix] Queue prev/next track

4.90.19
- [Fix] YouTube comment parsing
- [Fix] Up and Down change volume
- [Fix] Default URL input for playlists tab

4.90.18
- [Fix] Registry errors
- [Fix] YouTube comment parsing

4.90.17
- [Fix] Deleted video handler

4.90.16
- [Fix] Playlist name input

4.90.15
- [Fix] GUI queue

4.90.14
- [Fix] Sorting (DND) files
- [Fix] Spotify tracks

4.90.13
- [Fix] Improve chapter parsing

4.90.12
- [Fix] Proxy errors

4.90.11
- [Fix] Persistent queue

4.90.10
- [Fix] Deezer support

4.90.9
- [Fix] URL added feedback - Italian translation

4.90.8
- [UI] URL added feedback

4.90.7
- [Fix] System audio streaming related

4.90.6
- [Fix] YouTube comment parsing
- [Fix] Dynamic artwork

4.90.5
- [Fix] Better internal clocks
- [Fix] Mini-mode

4.90.4
- [Fix] Removed blocking logic

4.90.3
- [Fix] Main GUI

4.90.2
- [Fix] Library tab

4.90.1
- [Fix] YouTube playlists

4.90.0
- [Feat] Drag and Drop

4.89.0
- [Feat] Smart URL F-FWD and RWD

4.88.0
- [Feat] Metadata setter

4.87.6
- [Fix] Playlists tab

4.87.5
- [Fix] Playlists tab

4.87.4
- [Feat] Twitch support

4.87.3
- [Fix] Playlists tab
- [Fix] Move to next queue

4.87.2
- [Fix] Volume slider
- [Fix] URL resumption after long time
- [Fix] Prevent sleep after resuming playback

4.87.1
- [Fix] Multiple URL input support

4.87.0
- [Feat] Smart queue (auto skip)

4.86.6
- [Fix] Mini-mode

4.86.5
- [Fix] Library tab

4.86.4
- [Fix] Fix web GUI

4.86.3
- [Fix] Library sorting

4.86.2
- [Fix] Web GUI search

4.86.1
- [Optimization] Library table

4.86.0
- [UI] Added library tab
- [UI] Improve multi-selections

4.85.2
- [UI] Better (rounded) buttons
- [UI] Web GUI playlist options
- [UI] Web GUI file options

4.85.1
- [Fix] Deezer support

4.85.0
- [Fix] Registry
- [Feat] URL Protocol music-caster

4.84.3
- [Fix] Audio Player

4.84.2
- [Fix] Playlist format
- [Fix] Folder and file actions
- [UI] Playlist metadata scanning
- [Optimization] Spotify support

4.84.1
- [Fix] Resume playing with keyboard
- [Fix] Mini-mode with unknown metadata
- [Fix] Shuffle
- [Fix] Playing URL's

4.84.0
- [Feat] Deezer support
- [Fix] Spotify album support
- [Fix] System audio
- [Fix] Move tracks within next queue

4.83.0
- [Feat] Locate tracks in playlists
- [Feat] Added option to remember selected folder
- [Optimization] Spotify URL
- [Fix] YouTube/Soundcloud playlist/set support

4.82.14
- [Fix] Better persistent queue
- [Fix] Web GUI translation
- [Fix] M3U(8) File Handler

4.82.13
- [Fix] Web GUI auto-fresh

4.82.12
- [UI] Translation framework

4.82.11
- [Fix] Play URL double count

4.82.10
- [Fix] Main GUI when repeat is disabled

4.82.9
- [Fix] URL support
- [Fix] Soundcloud support

4.82.8
- [Fix] URL support

4.82.7
- [Fix] Tray select files
- [Fix] Metadata error
- [Fix] Commandline arguments
- [Optimization] Lowered CPU usage
- [Feat] Queue library always

4.82.6
- [Fix] Delete old files

4.82.5
- [Fix] Sharp noise when playing audio
- [Fix] Unknown album errors

4.82.4
- [Fix] Spotify handling

4.82.3
- [Fix] Unknown property bugs
- [Fix] Better web GUI

4.82.2
- [Fix] Prevent Windows from sleeping

4.82.1
- [Fix] M3U support

4.82.0
- [Feat] M3U(8) beta support
- [Fix] Translations
- [Fix] Playlists tab

4.81.14
- [Fix] URL Tab auto paste
- [UI] Playlists tab

4.81.13
- [Fix] Translations
- [Fix] Web GUI auto reload

4.81.12
- [Fix] Web GUI

4.81.11
- [Fix] URL expiry

4.81.10
- [UI] Added Italian translation

4.81.9
- [Fix] Local url playback
- [Fix] Improve YouTube local playback quality
- [Fix] Instance resolver

4.81.8
- [Fix] Locate uri

4.81.7
- [Fix] Locate uri

4.81.6
- [Fix] Scan queued urls

4.81.5
- [UI] Clicking title/artist/album text opens url or explorer
- [Fix] Locate track for mini-mode

4.81.4
- [Fix] Spotify URL

4.81.3
- [Fix] Spotify URL

4.81.2
- [Fix] Spotify URL

4.81.1
- [Fix] Spotify URL

4.81.0
- [Feat] Added Spotify support

4.80.6
- [Fix] URL metadata

4.80.5
- [Fix] Persistent URL's

4.80.4
- [Fix] Web GUI

4.80.3
- [Optimization] Blazing fast startup + open GUI

4.80.2
- [Fix] Vertical GUI size
- [Fix] Repeat button tooltip

4.80.1
- [Fix] Missing translation
- [Fix] High pitch noise
- [Fix] Locate track
- [Feat] Add option to pick language
- [UI] Improved fonts and UI looks

4.80.0
- [Feat] Play URL on local device
- [Fix] Tray menu

4.79.5
- [Fix] Translation framework

4.79.4
- [UI] Added Spanish translation
- [Fix] Translation framework
- [Fix] Metadata

4.79.3
- [UI] Web GUI improvements

4.79.2
- [Optimization] Selecting from queue

4.79.1
- [Fix] Auto update notification

4.79.0
- [Feat] Added option to hide queue index
- [Feat] Web GUI interactive queue
- [UI] Better queue
- [UI] More intuitive mini-mode behaviour
- [UI] Integrated URL window with main window
- [Optimization] Moving tracks

4.78.1
- [Fix] Web GUI

4.78.0
- [UI] Created framework for translations

4.77.0
- [Feat] Support for YouTube playlists and SoundCloud Sets
- [Fix] Playing YouTube links

4.76.4
- [Fix] Folder browse and playlists add tracks

4.76.3
- [Fix] Tray not showing up

4.76.2
- [Fix] Get latest release

4.76.1
- [Fix] Registry error

4.76.0
- [Feat] Move track in queue to next up
- [UI] Better queue button
- [Optimization] Tray has its own process to reduce drag stuttering and high cpu usage

4.75.1
- [Fix] Album and Title alignment resizing window

4.75.0
- [Feat] Live shuffle and un-shuffle the queue using the shuffle button
- [Optimization] Using deques instead of lists for internal queues (faster for massive queues)

4.74.28
- [Fix] Sorting improvements
- [Fix] Improved Web GUI searching
- [Fix] Improved Web GUI file display

4.74.27
- [Fix] No space error handling while saving settings

4.74.26
- [Fix] Album art display

4.74.25
- [Fix] Better download error handling

4.74.24
- [Fix] Playlists

4.74.23
- [Fix] Better scrubbing

4.74.22
- [Fix] Better scrubbing

4.74.21
- [Fix] Better image handling

4.74.20
- [Fix] Folder cover override UX

4.74.19
- [Fix] Improved binaries

4.74.18
- [Fix] Web GUI timer options

4.74.17
- [Fix] Removed internet available calls

4.74.16
- [Fix] Error handling

4.74.15
- [Fix] Loading settings

4.74.14
- [Fix] Wav file error

4.74.13
- [Fix] Loading numerical settings

4.74.12
- [Fix] Saving settings

4.74.11
- [Fix] Populate queue on startup
- [Fix] Loading settings
- [Fix] Saving settings
- [Fix] Improve web GUI

4.74.10
- [Fix] Improved natural sort

4.74.9
- [Fix] Metadata scanning for folders

4.74.8
- [Fix] Add folder in settings
- [Fix] Fixed Portable.zip compression

4.74.7
- [Fix] Playlist tab

4.74.6
- [Fix] Mini-mode

4.74.5
- [Fix] Mute icon staying when slider is moved

4.74.4
- [Fix] Shuffle and repeat off by default

4.74.3
- [Fix] Timer tab

4.74.2
- [UI] Sort playlists alphabetically
- [UI] Open Changelog file from settings
- [UI] Settings icons
- [Fix] Mute button starting tooltip

4.74.1
- [Fix] Mini-mode

4.74.0
- [UI] Show album name in now playing
- [Optimized] Metadata scanning

4.73.8
- [Fix] Volume control on startup
- [Fix] Volume control in GUI

4.73.7
- [Fix] Hot-key window focus

4.73.6
- [Fix] Discord presence errors

4.73.5
- [Optimized] Faster GUI activation via hot-key

4.73.4
- [Fix] Web shuffle
- [Fix] Main window activation through second instance

4.73.3
- [Fix] Tray icon

4.73.2
- [Fix] Stops at the end of tracks

4.73.1
- [UI] Improved mini-mode

4.73.0
- [Feat] Added shuffle to controls

4.72.1
- [Fix] Random stops

4.72.0
- [Feat] Added setting to disable folder scan

4.71.43
- [Fix] Removed default folder re-addition

4.71.42
- [Fix] Switching device error
- [Fix] Cleaner code

4.71.41
- [Fix] Cleaner code
- [Feat] Improved privacy by hashing MAC

4.71.40
- [Fix] Play from explorer
- [Fix] Album art issues

4.71.39
- [Fix] Chromecast not connected case

4.71.38
- [Fix] Resume after long pause
- [Fix] Random stopping

4.71.37
- [Fix] Web GUI
- [Fix] Improved GUI performance when playing non-library files
- [Fix] Code quality

4.71.36
- [Fix] Playlist tab moving a track up or down

4.71.35
- [Fix] Thumbnail fetching
- [Fix] Album art resizing issue
- [Fix] Portable auto-update on cancel
- [Fix] Invalid WAV files

4.71.34
- [Fix] Discord RPC error

4.71.33
- [Fix] Debugging keyboard errors

4.71.32
- [Fix] Better web error handling

4.71.31
- [Fix] Fix build

4.71.30
- [Fix] Improve artist parsing for metadata that uses '/' as a separator

4.71.29
- [Fix] Ctrl + # in mini mode

4.71.28
- [Fix] Fix using j,k,l on playlist tab
- [Fix] Fix using k to pause in mini-mode
- [Fix] Improve typing speed for main window

4.71.27
- [Fix] Delete playlist behaviour

4.71.26
- [Fix] Removing current track from queue
- [Fix] Set timer with HH:MM

4.71.25
- [Optimized] Caching

4.71.24
- [UI] Improved tray menu

4.71.23
- [Fix] Double Digit Track Numbers

4.71.22
- [Fix] Exit

4.71.21
- [Fix] Removed a dependency

4.71.20
- [Fix] Pressing Enter while using mini-mode

4.71.19
- [Fix] Improved mini-mode border padding

4.71.18
- [Web] Added version to web GUI settings

4.71.17
- [Fix] Better timing
- [Fix] Random stopping
- [Fix] Playing YouTube links
- [Fix] Playing tooltip

4.71.16
- [Fix] GUI volume slider not updating

4.71.15
- [Fix] Switching devices while paused

4.71.14
- [Fix] Window Icon

4.71.13
- [Fix] Web GUI search bar

4.71.12
- [Fix] files.html

4.71.11
- [Fix] Instance checker

4.71.10
- [Fix] Instance checker

4.71.9
- [Fix] Fixed playing youtube links
- [Fix] Changed locate file to locate track
- [Fix] Added save icon to playlist tab

4.71.8
- [Fix] Patched more errors

4.71.7
- [Fix] Update youtube-dl

4.71.6
- [Fix] Improved error handling

4.71.5
- [Fix] Improved logging

4.71.4
- [Fix] Improved logging

4.71.3
- [Fix] Auto-update True by default
- [Fix] Better default music folder

4.71.2
- [Fix] Main GUI

4.71.1
- [Fix] Web GUI play file
- [Fix] Web GUI styling
- [Fix] Web GUI view files

4.71.0
- [Feat] Added option to reverse play next behaviour

4.70.11
- [Fix] Handle no space bug

4.70.10
- [Fix] Web GUI improvements

4.70.9
- [Fix] Remove old installed files

4.70.8
- [Fix] Web GUI shuffle option

4.70.7
- [Fix] Fix log lines in email

4.70.6
- [Fix] Playlists tab

4.70.5
- [Fix] Queue from explorer

4.70.4
- [Fix] Rescan library from web GUI

4.70.3
- [Fix] Folder cover art

4.70.2
- [Fix] Reduce size

4.70.1
- [Fix] Track numbering

4.70.0
- [Feat] Added more to Web GUI
- [Feat] Added timer options to Web GUI
- [Fix] Web GUI resume
- [Fix] Man window tabs order

4.69.0
- [Feat] Playlist Tab (play, queue, edit)
- [Feat] Removed playlist related windows
- [Feat] Playlists support URLs (youtube and soundcloud)
- [Fix] Better email link

4.68.3
- [Fix] Folder dir as album cover
- [Fix] Improved error handling

4.68.2
- [Fix] Track number not showing

4.68.1
- [Fix] Settings field

4.68.0
- [Feat] Added format template in settings.json
- [Fix] Queue formatting

4.67.0
- [Feat] Queue all button
- [Feat] Show track number
- [Fix] Play file queueing 1+ files
- [Fix] Enable folder cover override
- [Fix] Disable folder context menu

4.66.0
- [Feat] Exit command
- [Feat] Shutdown API

4.65.24
- [Fix] Random crashes
- [Fix] Discord Rich Presence

4.65.23
- [Fix] Improved command-line argument updating

4.65.22
- [Fix] Play / queue from explorer

4.65.21
- [Feat] Command line supports playlist names

4.65.20
- [Fix] Playing at launch
- [Fix] Better local volume control

4.65.19
- [Fix] Play / queue from explorer for multiple files

4.65.18
- [Fix] Play / queue from explorer

4.65.17
- [Feat] Folder cover override option
- [Feat] Queue from explorer
- [Fix] Remove folder context menu
- [Fix] Play from explorer

4.65.16
- [Fix] Added first run notification

4.65.15
- [Fix] Improved command line

4.65.14
- [Fix] Album art bugs

4.65.13
- [Fix] Main window queue bug

4.65.12
- [Fix] .wav file bug

4.65.11
- [UI] Improved queue look

4.65.10
- [Fix] Better folder action sorting

4.65.9
- [Fix] Discord RPC

4.65.8
- [Fix] Antivirus False Positives

4.65.7
- [Misc] Updated icon

4.65.6
- [Fix] Play URL Next from tray if something is playing

4.65.5
- [Fix] Validate color codes in settings

4.65.4
- [Fix] Validate color codes in settings

4.65.3
- [Fix] Switch device LIVE audio

4.65.2
- [Fix] Roll back multi folder dialog

4.65.1
- [Fix] Exit behaviour

4.65.0
- [Feat] URL actions links pasted by default

4.64.25
- [Feat] Multi dir support
- [Fix] Weird looking folder selection

4.64.24
- [Fix] File action GUI queue population (not noticeable)

4.64.23
- [Fix] Getting files in folders
- [Fix] Folder action GUI queue population

4.64.22
- [Fix] Switching to local device

4.64.21
- [Fix] Random stopping

4.64.20
- [Fix] Show default art when stopped

4.64.19
- [Fix] Background tasks fixes
- [Fix] YouTube thumbnails

4.64.18
- [Fix] Update on exit

4.64.17
- [UI] Better Mini Mode
- [Optimized] Portable updater in C#
- [Fix] Double Instance (Faster now too)

4.64.16
- [Fix] Update on exit

4.64.15
- [Fix] Main GUI length parameter

4.64.14
- [Fix] Better logging

4.64.13
- [Fix] Better logging (smaller log file)

4.64.11
- [Fix] Premature stopping

4.64.10
- [Fix] Better logging

4.64.9
- [Fix] !show album art

4.64.8
- [Fix] Album art mini mode
- [Fix] Persistent Queues

4.64.7
- [Fix] Automated uploading

4.64.6
- [Fix] Improved local audio player
- [Fix] Improved logging

4.64.5
- [Fix] Offline playback
- [Fix] Starting with arguments
- [Fix] Settings modal Web GUI

4.64.4
- [Fix] Improved behaviour with unresponsive devices pt. 1
- [Fix] Improved debugging

4.64.3
- [Fix] Better exit behaviour

4.64.2
- [Fix] Main GUI queue rendering
- [Fix] Log file location

4.64.1
- [Fix] Main GUI queue rendering

4.64.0
- [Feat] Save queue as a playlist
- [Feat] Update on exit
- [Feat] Notify when updates available
- [UI] Better play URL window
- [UI] Move Mini Mode button above clear queue
- [Optimized] Better next and prev track from keyboard shortcuts
- [Fix] Mini-mode keyboard shortcuts
- [Fix] Mini-mode not respecting show album art setting
- [Fix] Stop from the tray

4.63.11
- [Fix] Reset progress
- [Fix] Switching device bug
- [Fix] Improved logging

4.63.10
- [Fix] Better keyboard pause/resume handling
- [Fix] Save Mini-mode position
- [Fix] Progress bar text
- [UI] Converted 3 buttons into images

4.63.9
- [Fix] Main Window UX

4.63.8
- [Fix] Mini mode sliders
- [UI] Check for updates periodically

4.63.7
- [UI] Playlist drop-down only shows if playlists exist
- [UI] Better Vertical GUI
- [Fix] No album art setting

4.63.6
- [Fix] Volume scrubbing in mini mode

4.63.4
- [Fix] Metadata again
- [Fix] If files don't exist
- [Fix] Live Audio

4.63.3
- [Fix] Seeking in mini-mode
- [Fix] Metadata

4.63.2
- [Fix] Play tracks

4.63.1
- [Fix] Settings tooltip

4.63.0
- [Fix] Playing an updated file
- [UI] Vertical UI
- [UI] Mini mode UI
- [UI] Album art is optional

4.62.2
- [Fix] Main GUI

4.62.1
- [Fix] Live Audio uses default output device now
- [Fix] Live Audio follows default output device
- [Fix] Stop() fix

4.62.0
- [UI] Album art in Main Window
- [UI] Better buttons with images

4.61.1
- [Fix] GUI when playing live audio

4.61.0
- [Feat] Cast Live System Audio

4.60.8
- [Web GUI] Added "Refresh Devices" to settings modal
- [UI] More tray play URL options
- [Optimized] Reduced memory footprint
- [Optimized] File indexing
- [Fix] Play folder window
- [Fix] Corrupt file handling
- [Fix] Internet cutting out scenarios

4.60.7
- [Fix] Pausing / Resuming local playback bugs
- [Fix] Play file with new instance

4.60.6
- [Fix] Refresh Devices

4.60.5
- [Fix] Maxing volume on local device

4.60.4
- [Fix] Main window queuing

4.60.3
- [Fix] Mute behaviour

4.60.2
- [UI] Better UI styling
- [Fix] Timer error text
- [Fix] Support for .mpeg files

4.60.1
- [Fix] Discord Presence Errors
- [Fix] Better Registry Modifications

4.60.0 (August 2020)
- [Feat] Register Music Caster in the registry as a program to open audio files and folders

4.59.5
- [Optimized] Delete old files when updating

4.59.4
- [Optimized] images folder no longer needed
- [Fix] Album art not showing on devices

4.59.3
- [UI] Slightly better icon
- [UI] New start menu tile (matte black style)
- [UI] Renamed Song to Track
- [UI] Better queue modification experience
- [Fix] Move track up in the queue
- [Fix] Web GUI selected device

4.59.2
- [Dependency] Updated youtube-dl

4.59.1
- [Fix] Change device

4.59.0
- [Feat] Better progress bar
- [Feat] Support for all formats locally (thanks to VLC bindings)
- [Feat] 32-bit support

4.58.0
- [Feat] See and download available files through /files/

4.57.0
- [Feat] Play URL supports SoundCloud
- [Fix] Play URL
- [Fix] WAV files

4.56.4
- [Fix] Music Queue with non-existent files

4.56.3
- [Fix] Switching device after a long time

4.56.2
- [Fix] Cancelling file selector closes the window

4.56.1
- [UI] Made buttons wider

4.56.0
- [UI] Merged timer window as a tab on the main window
- [UI] Added Queue URL to the main window
- [UI] Added Ctrl + {1, 2, 3} for main window tab control
- [UI] Use Ctrl + Shift + Alt + M to launch main window
- [UI] Added fast-forward, rewind, and pause keyboard shortcuts (L, J, K)
- [UI] Added more play options to the main window
- [Optimized] Cached YouTube URLs
- [Optimized] Even faster startup
- [Fix] Made populate and save session queues mutually exclusive
- [Fix] Queue files
- [Fix] Get metadata
- [Fix] Discord rich presence
- [Fix] Playlist Selector and Editor

4.55.0
- [Optimized] Reduced installation size
- [Fix] Error handling
- [Fix] Resume playback after long time

4.54.8
- [Fix] Auto-updating

4.54.7
- [UI] Sort cast devices by groups first

4.54.6
- [Fix] Cast groups detection (zeroconf == 0.24.5)

4.54.5
- [Fix] Exit after downloading update

4.54.4
- [Fix] No metadata WAV files

4.54.2
- [Fix] Auto-update

4.54.1
- [Fix] Auto-update
- [Feat] Better url support

4.54.0
- [Feat] Change device via web GUI

4.53.1
- [Optimized] Faster startup time (-70%)
- [Fix] Auto update

4.53.0
- [UI] Removed ugly border from buttons
- [UI] Merged Tab's 1 and 2 for Main Window
- [UI] Merged Settings with Main Window
- [Optimized] Settings Window
- [Optimized] Faster update checking
- [Optimized] Better Portable Updater
- [Fix] Queue file
- [Fix] Save window positions

4.52.0
- [Feat] Play URL works alright now (youtube only)
- [Fix] Web shortcut icons

4.51.3
- [Optimized] Main Tray
- [UI] Swapped "Controls" and "Play" in the tray menu
- [UI] New text color #d7d7d7 (either delete settings.json or enter it manually)
- [Fix] Startup error if MC is already running
- [Fix] Population/Save queue will load queue now

4.51.2
- [Fix] Settings loading

4.51.1
- [Fix] Main window closing after cancelling  queue file

4.51.0
- [Feat] Populate queue on startup
- [Feat] Save queue between sessions

4.50.2
- [Fix] Startup error

4.50.1
- [Optimized] Threaded file selection windows
- [Fix] Play Folder

4.50.0
- [Feat] Music library will build in the background
- [Feat] Added "Refresh Library" to tray

4.49.5
- [Fix] Better update error handling

4.49.4
- [Fix] Pause bug
- [Fix] Main Window file picking close

4.49.3
- [Fix] Tooltips
- [Fix] Play All

4.49.2
- [Fix] Discord rich presence bug

4.49.0
- [Feat] Added do nothing to timer
- [Fix] .opus files

4.48.1
- [Fix] Icon Quality

4.48.0
- [Feat] Improved Quality of Icon

4.47.1
- [Fix] Forgot to update version

4.47.0
- [Feat] Reorganized Main Window Tab 2
- [Feat] Added support for .wma files (cast only)

4.46.0
- [Feat] Queue file now supports multiple files
- [Feat] Play Youtube links (EXPERIMENTAL)
- Does not support pause, skip, or any repeating
- [Optimized] Settings file loading

4.45.0
- [Feat] Scroll to the playing song for Web GUI music queue

4.44.0
- [Feat] Added support for .wav
- [Fix] Better metadata logic

4.43.3
- [Fix] Web GUI style fix

4.43.2
- [Fix] Volume control actually works now

4.43.1
- [Fix] Auto-update will be disabled if something goes wrong while updating

4.43.0
- [Feat] Added View queue to Web GUI
- [Feat] Added Volume Control to Web GUI

4.42.2
- [Feat] Changed default setting for discord RPC to false
- [Feat] Web GUI title now includes the PC's name

4.42.1
- [Fix] Shortcut creation

4.42.0
- [Feat] Added setting UI to Web GUI

4.41.3
- [Fix] Music Queue double click to play

4.41.2
- [Fix] Web GUI

4.41.1
- [Feat] Web GUI on mobile doesn't make the keyboard come up

4.41.0
- [Feat] Added support for {*.flac,*.m4a,*.mp4,*.aac,*.ogg,*.opus,*.wav} (for non-local devices)

4.40.0
- [Optimized] Reduced startup time by ~2 seconds
- [Optimized] No more copying files from different drives, the solution was so simple but took ~10 months to find!
- [Fix] Stopped Music Caster collisions between two devices on the same network
- [Security] Server will only serve music files and only if the correct UUID is passed

4.39.5
- [Optimized] Instance checker, startup time reduce by ~9 seconds!
- [Fix] Reverted how keyboard commands work

4.39.4
- [Fix] Music queue remove

4.39.3
- [Fix] Music queue

4.39.2
- [Fix] Music queue
- [Fix] Better email hyperlink

4.39.1
- [Fix] Web GUI

4.39.0
- [Feat] Added search to web GUI
- [Feat] Better mobile web GUI

4.38.1
- [Fix] Handled more metadata errors
- [Fix] Installer installs all files now

4.38.0
- [Feat] Can now play files from the web GUI
- [Fix] Removed duplicate code
- [Fix] Better Chromecast detection

4.37.0
- [Feat] Added notification if MC was updated
- [Feat] Double click a song in the music queue to play the song
- [Feat] Locate File moved to second tab
- [Fix] Opening Music Caster a second time

4.36.1
- [Fix] Handled HeaderNotFoundError

4.36.0
- [Optimized] Embedded Updater (+ updating notification)
- [Optimized] Less cpu usage when idle

4.35.0
- [Optimized] Image assets
- [Feat] Main GUI volume image acts like mute/unmute button
- [Fix] Settings window error

4.34.2
- [Fix] Add folder works with save window positions

4.34.1
- [Fix] Errors are actually sent to me now

4.34.0
- [Feat] Added repeat off option (click repeat button 2x)
- [Feat] Added "Save window positions" setting
- [Optimized] Custom Chromecast finder (email me if MC doesn't detect any chromecasts)
- [Optimized] Better CPU usage
- [Optimized] More threading

4.33.3
- [Fix] lag caused by Discord RPC

4.33.2
- [Feat] Added tooltips for the QR Code and web GUI link
- Removed "Email:" text and added tooltip to the email link

4.33.1
- [Fix] Discord RPC bug when using web GUI

4.33.0
- [Feat] Added QR Code to settings for quick access to the web GUI
- [Fix] Error when handling exception
- [Fix] Web GUI when no file is playing

4.32.3
- [Fix] Updater.exe AGAIN
- [Feat] Added basic optional Discord RPC integration
- [Fix] playlist editor window (window position is not saved for now)

4.31.0
- [Optimized] Executables size and startup time
- [Optimized] Portable updater now in C#
- [Feat] Window positions are now remembered
- [Feat] Added some tooltips
- [Fix] tray menu updates again if repeat is pressed

4.30.4
- [Fix] Main GUI and shortcut creation

4.30.3
- [Fix] Playlist editor arrow key handler

4.30.2
- [Feat] Beefed up error messages that are sent to me
- [Fix] Better temp music folder handling

4.30.1
- [Feat] You can now open a file with Music Caster
- File handling in the context menu

4.29.0
- [Feat] Added "Queue Folder" to the main GUI (Tab 2)
- [Feat] Added "Clear Queue" to main GUI (Tab 2)
- [Fix] Handled ID3 error when playing files
- Reordered tray play menu

4.28.0
- [Feat] Added play folder to tray (found under tray play menu)
- Moved Playlists to Play tray menu

4.27.4
- [Fix] Fixed Web GUI album art

4.27.3
- [Fix] Volume scrolling on main window popup
- [Error handling] Updater raises fewer errors
- [Fix] Error handling bug

4.27.2
- [Fix] Better handling of errors
- [Fix] Play file works even without any folders

4.27.1
- Better duplicate detection
- Fixed compile all songs bugs

4.27.0
- Select device no devices Fix
- WEB GUI fix
- Better Auto-update

4.26.2
- Fixed bug

4.26.1
- Removed irrelevant data that was sent to me (install folder)
- Added POST request so I know how many users use Music Caster

4.26.0
- Better Play All (+ fixed a bug I created in the process before releasing)
- Because of this bug, I have added a helper function to time my functions
- Optimized `change_settings` (faster skips, although negligible)
- Building a GUI tab to show all music
- Update Web UI (looks good on mobile now, hard to access though)
- Changed setup name
- Fixed auto-update error handling (ironic)
- Fixed volume not changing when scrolling bug

4.25.0
- Added locate file option to main GUI (folder icon)
- Fixed play file next bug
- Updated timer window GUI

4.24.4
- Fixed volume scrolling behavior
- Removed volume from settings window (use the main window instead)

4.24.3
- Better error handling
- Added more error logging information

4.24.2
- Scrolling in the  settings or main window by default changes volume

4.24.1
- Fixes

4.24.0
- Web GUI fully functional
- ID3 tags reading fix
- Fixed song position bug

4.23.0
- Volume and scrubbing now support mouse scrolling!
- Settings window "Enable notifications" -> "Notifications"
- Tray tooltip now says "Download Update..." instead of disappearing
- Fixed restart on error?

4.22.4
- Fixed IndexError's when no file sin queue

4.22.3
- Fixed bugs to do with changing the music queue

4.22.2
- Fixed web access
- Fixed song position bug (maybe)
- Fixed port conflict bug

4.22.1
- Added button text color option
- Fixed bug (reactivating a window through the tray)
- Fixed volume slider bug

4.22.0
- New main window update
- Added image for the repeat button
- Added volume control
- Added music queue control
- GUI volume slider's update when the volume is updated through the home app
- Added a checkmark next to repeat tray menu if the repeating song is True
- Main GUI is now accessible to all! Just click the icon
- Change accent color
- Added settings.json options to change accent color (requires restart)
- Fixed keyboard shortcut option

4.21.2
- bigger try-except to catch more bugs

4.21.1
- fixed bug where "open settings.json" would fail if the user had no JSON file handler

4.21.0
- Errors are now sent to me automatically
- Information that is shared with me
    - Music Caster Version
    - OS platform and version
    - Traceback error message
    - (see bottom of music_caster.py)
- Updated error.log to include Music Caster's version

4.20.1
- Fixed no response bug when you switch devices while song is paused

4.20.0
- Works if song is scrubbed from home app
- Better song timing (when the next song will play)

4.19.1
- FIXED: now playing text (MAIN GUI) was displaying song when playback was stopped
- Can now use Up/Down and Page Up/Down to move through music queue GUI (EXPERIMENTAL)
- CPU won't spike anymore for a few seconds when trying to play something on cast device

4.19.0
- Changed tooltip to have song info
- Tray shows which device is currently selected

4.18.6
- Support for subfolders when using "Play All"

4.18.5
- Fixed bug where all devices showed the same name

4.18.4
- Minor refactoring + change in selecting device logic
- Change in chromecast devices sorting criteria (alpha, then UUID)

4.18.3
- Sorted device list alphabetically with (local device) being the exception

4.18.2
- Fixed multiple Chromecasts bug

4.18.1
- Fixed timer to work with improved performance
- Changed "Stop Timing" to "Cancel Timer"

4.18.0
- Added "Locate File" feature to Controls tray menu
- Added changelog

4.17.27
- Streamlined controls
- New EXPERIMENTAL keyword in settings.json (set to true for main window access)

4.17.26
- Fixed bug to do with wanting to create/edit a playlist name with 'q'
- Streamlined Play options (Play File, Play a File Next -> Play File Next, Play All) to be in a cascaded menu
- Improved performance (theoretically) by not reading the tray if the program is not in use
- Edited settings window to not have a copy button anymore, makes life harder than just clicking the email hyperlink
- Updated GUI library to the latest
- Fixed bug with main GUI
- Main GUI is almost done, test it out by putting `"EXPERIMENTAL": True` in `settings.json`

4.17.25
- Fixed chromecast buffering bug that would screw up when the next song would start playing

Wow you scrolled all the way to the end.
Pre v4.17.25 changes found here https://github.com/elibroftw/music-caster/releases?after=v4.17.25


================================================
FILE: Dockerfile
================================================
# this images allows building music caster into a folder that can be run
#
FROM fedora:latest
ENV PY=python3.14
ENV PIP_ROOT_USER_ACTION=ignore
# install required packages
RUN dnf upgrade -y && dnf install -y \
    $PY $PY-devel $PY-virtualenv python3-devel python3-tkinter python3-pyaudio \
    dnf-plugins-core libappindicator-gtk3 binutils
# install some dependencies here to reduce the dependencies installed at run time
COPY requirements.txt build_files/ music-caster/
RUN $PY -m pip install --upgrade pip && $PY -m pip install pyaudio
RUN cd music-caster && $PY -m pip install --upgrade -r requirements.txt
RUN rm -rf ./music-caster
# when running this image, need to mount the work directory to /var/music-caster
CMD if [ ! -d /var/music-caster ] ; then git clone https://github.com/elibroftw/music-caster/ /var/music-caster ; fi && cd /var/music-caster && \
    $PY -m pip install --upgrade pip && \
    $PY -m pip install --upgrade -r requirements.txt && \
    $PY -m pip install -r requirements-dev.txt && \
    $PY -O -m PyInstaller build_files/onedir.spec && \
    echo "done! check your dist folder"
    # TODO: create AppImage from dist/Music Caster OneDir


================================================
FILE: LICENSE
================================================
Copyright © Elijah Lopez

You cannot sell Music Caster, unless there's an at least 50% royalty payment to the majority contributor (Elijah Lopez).
Any modified version of Music Caster cannot be sold, unless again there's a 50%+ royalty
to the majority contributor of the original version (Elijah Lopez).
Any use of source code from Music Caster for the purpose of selling a competitor to Music Caster
shall also have to meet a 50%+ royalty payment to the projects majority contributor (Elijah Lopez).

(Obviously) No contributor of Music Caster is responsible for any
damages caused by Music Caster, if any occur. This is not confirmation that damages are even remotely likely,
it is simply a statement to protect contributors of this FREE and OPEN software.

This license cannot restrict you from modifying the source code, compiling, and running it on your machine.
This license also cannot restrict you from distributing PATCHES for Music Caster.

This license is to be understood by spirit and motivation not by its letter.


================================================
FILE: README.md
================================================

<p align="center"><img src="https://user-images.githubusercontent.com/21298211/171323258-5818355a-2c55-444b-8d0d-b0e3feee36e4.png" /> </>

[![GitHub Releases](https://img.shields.io/github/downloads/elibroftw/music-caster/latest/total?color=blue&label=github%20downloads%40latest&style=for-the-badge)](https://github.com/elibroftw/music-caster/releases/latest)
[![Source Forge](https://img.shields.io/sourceforge/dt/music-caster?color=orange&label=SourceForge%20downloads&style=for-the-badge)](https://sourceforge.net/projects/music-caster/)

Music Caster is a modern music player with the ability to cast audio files, system audio, and URLs to Google Chromecasts, Google Home/Nest Minis, etc.

Display languages: English, German, Spanish, French, Italian, Dutch, Russian\*, and Ukrainian\*

Unique users as of April 23rd 2023: 3,800

[Screenshots](https://elijahlopez.ca/music-caster/)

### Donate or Translate

- monero:84PR6SkYd5zaFLKDjAFrQfbaAg2c7SV3q3XDZ15QCpEZUggrN4YzY7n8m9XC3deXjo41yWHTm1LrsUpPTYGnRQbD9Cwp8En
- [PayPal](https://www.paypal.me/elibroftw)
- [Translate](https://github.com/elibroftw/music-caster/issues/12#issuecomment-808658776) Music Caster to other languages

## Install

### [Windows Download Music.Caster.Setup.exe](https://github.com/elibroftw/music-caster/releases/latest)

- **IMPORTANT INFORMATION:** The tray icon will be in the tray, so you will need to move it to your taskbar
- Command line installation: `winget install "Music Caster"`
- [VirusTotal scan](https://www.virustotal.com/gui/file/40a1c61e5cb2c5eed714eb70bb84f138e9fd9742076ea665b4ac85fc8f372abf)
  - If Music Caster is auto-removed, open "Virus & threat protection", then "protection history," and restore all files related to Music Caster

### Linux

Not maintained, but I did get it to work on Ubuntu once. Music Caster is not straight forward to package, so you can invoke a sudo-free [install script](linux_install.sh).

```bash
mkdir -p ~/bin && git clone --depth 1 https://github.com/elibroftw/music-caster.git ~/bin/music-caster
cd ~/bin/music-caster
./linux_install.sh # use sudo for non-interactive install in case a dependency needs to be installed
```

## Demo

<a href="https://www.youtube.com/watch?v=5xwHkLPgvtQ" title="Music Caster Video Demo">
  <p align="center">
    <img width=75% src="https://img.youtube.com/vi/5xwHkLPgvtQ/maxresdefault.jpg" alt="Music Caster Video Demo Thumbnail"/>
  </p>
</a>

## Limitations

- Chromecasts only support the AAC version of WMA files
- Emojis might not work well. There's always settings.json + WEB GUI though
- [Road Map](https://github.com/elibroftw/music-caster/projects/1)

## Power User Features

- Global media hot-keys are supported
- Web GUI (QR code in Settings window)
- [Command Line Arguments](https://github.com/elibroftw/music-caster/wiki/Command-Line-Arguments)

Here are Music Caster specific keyboard shortcuts aside from the global media hot-keys.

| **Shortcut**           | **Window** | **Behaviour**                  |
|------------------------|------------|--------------------------------|
| Ctrl + Shift + Alt + M | Global     | Activate Main Window           |
| Ctrl + (Shift) + }     | Main       | Toggle mini-mode               |
| Esc                    | Main       | Close Window                   |
| Ctrl + Shift + Q       | Main       | Exit Program                   |
| Scroll                 | Main       | Volume and Progress Bar        |
| ⬆ / A                  | Main       | Decrease Volume by 5%          |
| ⬇ / D                  | Main       | Increase Volume by 5%          |
| #                      | Main       | Set Volume to # * 10%          |
| K                      | Main       | Pause / Resume / Start Playing |
| Shift + N              | Main       | Next Track                     |
| Shift + P / Shift + B  | Main       | Previous Track                 |
| J                      | Main       | Rewind 5 seconds               |
| L                      | Main       | Fast-forward 5 seconds         |
| Ctrl + R               | Main       | Cycle Repeat                   |
| Ctrl + M               | Main       | Mute                           |
| Ctrl + 1               | Main       | Go to Tab 1 (Queue)            |
| Ctrl + 2               | Main       | Go to Tab 2 (URL)              |
| Ctrl + 3               | Main       | Go to Tab 3 (Library)          |
| Ctrl + 4               | Main       | Go to Tab 4 (Playlists)        |
| Ctrl + 5               | Main       | Go to Tab 5 (Timer)            |
| Ctrl + 6               | Main       | Go to Tab 6 (Metadata)         |
| Ctrl + 7               | Main       | Go to Tab 7 (Settings)         |

### Editing `settings.json`

- I do not recommend editing unless you know what you are doing
- Music Caster will detect changes within 10 seconds of editing `settings.json`
- Some settings values are hidden from the GUI for good reason

## Data Collection / Privacy Policy

Below is the reasonable data that is collected when errors are encountered. I'm sure other programs collect
way more than necessary.

```python
# in handle_exception,
payload = {'VERSION': VERSION, 'FATAL': restart_program, 'EXCEPTION TYPE': exc_type.__name__,
           'LINE': exc_tb.tb_lineno, 'TRACEBACK': trace_back_msg, 'LOG': log_lines,
           'MQ[0]': playing_uri, 'PLAYING_STATUS': str(playing_status), 'DEVICE': device,
           'CWD': os.getcwd(), 'PORTABLE': not os.path.exists(UNINSTALLER),
           'MAC': hashlib.md5(get_mac().encode()).hexdigest(), 'OS': platform.platform(), 'TIME': current_time}
```

In addition, I collect MD5 hashed MAC addresses and IP addresses in a Google Excel Sheet.
Only I have access to this data, I will NEVER give it to anyone else. Will stop collecting analytics once I stop caring about the number of users.

- Hashed MAC so that I know how many users without knowing the actual MAC addresses
- IP because I can map out the IPs to a visual map to see where my users are located

[Developer Guide](https://github.com/elibroftw/music-caster/wiki/Developer-Guide)

### Linux Build Guide

- Define correct PY variable (requires rebuilding the image)
- Obtain the mc-builder Image
  - Option A: `docker pull elibroftw/mc-builder`
  - Option B: `docker build . -t elibroftw/mc-builder`
    - Remember to have Docker desktop/daemon running already
- Build source code using: `docker run --rm --volume .:/var/music-caster elibroftw/mc-builder`

### Virtualenv

You need to use the free threading version (python3.14).

```sh
python3.14 -m venv .venv
.venv\Scripts\activate     # Windows
source .venv/bin/activate  # Non-Windows
```

### Resources

- [Generate sound waves for any audio file](https://gist.github.com/elibroftw/0fc6ed102dbe3f99863829a7e989dcc2)

## Upgrading Python Version

Update the version found in the following files

- .github/workflows/build.yml
- Dockerfile
- README#virtualenv
- linux_install.sh
- scripts/debian-install.sh

1. Next, in a another directory clone PyInstaller like so: `git clone git@github.com:pyinstaller/pyinstaller.git`
2. We need to [build the bootloader](https://pyinstaller.org/en/stable/bootloader-building.html) ourselves to avoid being flagged by Anti Virus.
3. Run `py -m build` (not in the bootloader directory)
4. Compile [pyaudio_portaudio](https://github.com/elibroftw/pyaudio_portaudio)
5. After this, we need to update the requirements-dev.txt.
6. Create venv

From here, you can test the app in the newer version of Python (need to initialize a new virtualenv)

Lastly, build the app and submit it to Microsoft for false positive. I'm not sure when I'll buy a cert to ensure we don't encounter this again.


================================================
FILE: build.cmd
================================================
@echo off
set args=%1
shift
:start
if [%1] == [] goto done
set args=%args% %1
shift
goto start
:done
python -m venv .venv 2>NUL
".venv/Scripts/python.exe" build.py %args%


================================================
FILE: build.py
================================================
#!/usr/bin/env python3
import argparse
import glob
import math
import os
import platform
import shutil
import sys
import threading
import time
import traceback
import zipfile
from contextlib import suppress
from datetime import datetime
from pathlib import Path
from subprocess import DEVNULL, CalledProcessError, Popen, check_call, getoutput
from multiprocessing import freeze_support

if __name__ == '__main__':
    freeze_support()

from src.meta import VERSION, USING_TAURI_FRONTEND

# build constants
script_dir = Path(__file__).parent
SRC_DIR = script_dir / 'src'
DIST_DIR = script_dir / 'dist'
build_files = script_dir / 'build_files'
SETUP_OUTPUT_NAME = 'Music Caster Setup'
VERSION_FILE = build_files / 'mc_version_info.txt'
INSTALLER_SCRIPT = build_files / 'setup_script.iss'
PORTABLE_SPEC = build_files / 'portable.spec'
DAEMON_SPEC = build_files / 'daemon.spec'
ONEDIR_SPEC = build_files / 'onedir.spec'
UPDATER_SPEC_FILE = build_files / 'updater.spec'
CHANGELOG_FILE = script_dir / 'CHANGELOG.txt'
TKDND_FILES = (build_files / 'tkdnd2.9.2', build_files / 'TkinterDnD2')
UPDATER_MANIFEST_FILE = build_files / 'Updater.exe.MANIFEST'
UPDATER_ICO = build_files / 'updater.ico'
UPDATER_DIST = DIST_DIR / 'Updater.exe'
REQUIREMENTS_FILE = script_dir / 'requirements.txt'
REQUIREMENTS_DEV_FILE = script_dir / 'requirements-dev.txt'
SRC_FRONTEND = script_dir / 'src-frontend'

IS_VENV = sys.prefix != sys.base_prefix


class ProgressUpload:
    # 1MB chunk size
    def __init__(self, filename, chunk_size=1_024_000):
        self.filename = filename
        self.chunk_size = chunk_size
        self.file_size = os.path.getsize(filename)
        self.size_read = 0
        self.divisor = min(math.floor(math.log(self.file_size, 1000)) * 3,
                           9)  # cap unit at a GB
        self.unit = {0: 'B', 3: 'KB', 6: 'MB', 9: 'GB'}[self.divisor]
        self.divisor = 10**self.divisor

    def __iter__(self):
        progress_str = f'0 / {self.file_size / self.divisor:.2f} {self.unit} (0 %)'
        sys.stderr.write(f'\rUploading {self.filename}: {progress_str}')
        with open(self.filename, 'rb') as file_to_upload:
            for chunk in iter(lambda: file_to_upload.read(self.chunk_size),
                              b''):
                self.size_read += len(chunk)
                yield chunk
                sys.stderr.write('\b' * len(progress_str))
                percentage = self.size_read / self.file_size * 100
                completed_str = f'{self.size_read / self.divisor:.2f}'
                to_complete_str = f'{self.file_size / self.divisor:.2f} {self.unit}'
                progress_str = (
                    f'{completed_str} / {to_complete_str} ({percentage:.2f} %)'
                )
                sys.stderr.write(progress_str)
                sys.stderr.flush()
        sys.stderr.write('\n')

    def __len__(self):
        return self.file_size


def read_env(env_file='.env'):
    if not args.ci:
        with open(env_file, encoding='utf-8') as env_file:
            env_line = env_file.readline()
            while env_line:
                k, v = env_line.split('=', 1)
                os.environ[k] = v.strip()
                env_line = env_file.readline()
    return os.environ


def add_new_changes(prev_changes: str):
    changes = set(prev_changes.split('\n'))
    with open(CHANGELOG_FILE, encoding='utf-8') as _file:
        add_changes = False
        line = _file.readline()
        while line:
            line = line.strip()
            if line == VERSION:
                add_changes = True
            elif add_changes:
                if line == '':
                    break
                changes.add(line)
            line = _file.readline()
    if not add_changes:
        # TODO: generate changelog from commits
        #       could use AI/LLM for summarizing subjects
        #       see modern-desktop-app
        print(f'CHANGELOG does not contain changes for {VERSION}...')
        if args.ci:
            sys.exit(3)
        input('Press enter to try again...')
        return add_new_changes(prev_changes)
    return '\n'.join(sorted(changes, key=lambda item: item.casefold()))


def set_spec_debug(debug_option):
    for file_name in (ONEDIR_SPEC, PORTABLE_SPEC, UPDATER_SPEC_FILE):
        with open(file_name, 'r+', encoding='utf-8', newline='\n') as _f:
            new_spec = _f.read().replace(f'debug={not debug_option}',
                                         f'debug={debug_option}')
            new_spec = new_spec.replace(f'console={not debug_option}',
                                        f'console={debug_option}')
            _f.seek(0)
            _f.write(new_spec)
            _f.truncate()


def create_zip(zip_filename, files_to_zip, compression=zipfile.ZIP_BZIP2):
    with zipfile.ZipFile(zip_filename, 'w', compression=compression) as zf:
        for file_to_zip in files_to_zip:
            try:
                if type(file_to_zip) == tuple:
                    zf.write(*file_to_zip)
                else:
                    zf.write(file_to_zip)
            except FileNotFoundError:
                print(f'{file_to_zip} not found')


def update_versions(version):
    """Update versions of version file and installer script"""
    with open(VERSION_FILE, 'r+', encoding='utf-8', newline='\n') as version_info_file:
        lines = version_info_file.readlines()
        for i, line in enumerate(lines):
            if line.startswith('    prodvers'):
                version_tuple = ', '.join(version.split('.'))
                lines[i] = f'    prodvers=({version_tuple}, 0),\n'
            elif line.startswith('    filevers'):
                version_tuple = ', '.join(version.split('.'))
                lines[i] = f'    filevers=({version_tuple}, 0),\n'
            elif line.startswith("        StringStruct('FileVersion"):
                lines[
                    i] = f"        StringStruct('FileVersion', '{version}.0'),\n"
            elif line.startswith("        StringStruct('LegalCopyright'"):
                lines[
                    i] = f"        StringStruct('LegalCopyright', 'Copyright (c) 2019 - {YEAR}, Elijah Lopez'),\n"
            elif line.startswith("        StringStruct('ProductVersion"):
                lines[
                    i] = f"        StringStruct('ProductVersion', '{version}.0')])\n"
                break
        version_info_file.seek(0)
        version_info_file.writelines(lines)
        version_info_file.truncate()

    with open(INSTALLER_SCRIPT, 'r+', encoding='utf-8', newline='\n') as version_info_file:
        lines = version_info_file.readlines()
        for i, line in enumerate(lines):
            if line.startswith('#define MyAppVersion'):
                lines[i] = f'#define MyAppVersion "{version}"\n'
            elif line.startswith('OutputBaseFilename'):
                lines[i] = f'OutputBaseFilename={SETUP_OUTPUT_NAME}\n'
                break
        version_info_file.seek(0)
        version_info_file.writelines(lines)
        version_info_file.truncate()


def local_install():
    exe = os.getenv('LOCALAPPDATA') + '/Programs/Music Caster/Music Caster.exe'
    cmd = [
        str(DIST_DIR / 'Music Caster Setup.exe'),
        '/FORCECLOSEAPPLICATIONS',
        '/VERYSILENT',
        '/MERGETASKS="!desktopicon"',
    ]
    cmd.extend(('&&', exe))
    if not player_state.get('gui_open', False):
        cmd.append('--minimized')
    if player_state.get('status', 'NOT PLAYING') in ('PLAYING', 'PAUSED'):
        cmd.append('--start-playing')
        if player_state['status'] == 'PAUSED':
            cmd.append('--queue')
    if position := player_state.get('position', 0) > 0:
        cmd.append(f'--position={position}')
    Popen(cmd, shell=True)


def test(title, fn, assert_statement=False):
    try:
        if assert_statement:
            assert fn(), title
        else:
            fn()
    except Exception as _e:
        print('---')
        print('TEST FAILED', title)
        print('TEST TRACEBACK', traceback.format_exc())
        print('---')
        raise _e


def upgrade_yt_dlp():
    latest_ytdl = 'https://api.github.com/repos/yt-dlp/yt-dlp/commits/master'
    latest_mc = 'https://api.github.com/repos/elibroftw/music-caster/releases/latest'
    yt_dlp_master = requests.get(latest_ytdl).json()
    ytdl_publish = yt_dlp_master['commit']['author']['date']
    yt_dlp_release_time = datetime.strptime(ytdl_publish, '%Y-%m-%dT%H:%M:%SZ')
    mc_publish = requests.get(latest_mc).json()['published_at']
    mc_release_time = datetime.strptime(mc_publish, '%Y-%m-%dT%H:%M:%SZ')
    if mc_release_time < yt_dlp_release_time:  # latest yt-dlp not used in latest MC
        print('New YouTube-dl found, updating Music Caster version')
        # if youtube-dl was released after the latest music-caster, update version and publish
        maj, _min, fix = VERSION.split('.')
        fix = int(fix) + 1
        new_version = f'{maj}.{_min}.{fix}'
        with open('meta.py', 'r+', encoding='utf-8', newline='\n') as f:
            # VERSION = latest_version = '5.0.0'
            new_txt = f.read().replace(
                f"VERSION = latest_version = '{VERSION}'",
                f"VERSION = latest_version = '{new_version}'",
            )
            f.seek(0)
            f.write(new_txt)
        with open(CHANGELOG_FILE, 'r+', encoding='utf-8', newline='\n') as f:
            content = ''.join(
                (f.readline(), f'\n{VERSION}\n- Upgrade dependencies\n',
                 f.read()))
            f.seek(0)
            f.write(content)
        update_versions(new_version)
        # commit and push change
        from git import Repo

        repo = Repo('.git')
        repo.git.add(update=True)
        repo.index.commit('Upgraded yt-dlp')
        origin = repo.remote(name='origin')
        origin.push()


if __name__ == '__main__':
    start_time = time.time()
    starting_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
    os.chdir(starting_dir)
    # CONSTANTS
    YEAR = datetime.today().year

    parser = argparse.ArgumentParser(description='Music Caster Build Script')
    parser.add_argument(
        '--debug',
        '-d',
        default=False,
        action='store_true',
        help='build as console app + debug=True',
    )
    parser.add_argument(
        '--ver_update',
        '-v',
        default=False,
        action='store_true',
        help="Only update build files' version",
    )
    parser.add_argument(
        '--clean',
        '-c',
        default=False,
        action='store_true',
        help='Use pyinstaller --clean flag',
    )
    parser.add_argument(
        '--upload',
        '-u',
        '--publish',
        default=False,
        action='store_true',
        help='Upload and Publish to GitHub after building',
    )
    parser.add_argument(
        '--skip-build',
        '--no-build',
        default=False,
        action='store_true',
        help='Skip to testing / uploading',
    )
    parser.add_argument('--skip-tests',
                        '--no-tests',
                        '--st',
                        default=False,
                        action='store_true',
                        help='Skip testing')
    parser.add_argument(
        '--force-install',
        '-f',
        default=False,
        action='store_true',
        help='Force install after build',
    )
    parser.add_argument(
        '--deps',
        '--dry',
        default=False,
        action='store_true',
        help='does not modify anything',
    )
    parser.add_argument(
        '--test-auto-update',
        default=False,
        action='store_true',
        help='use if testing auto update',
    )
    parser.add_argument(
        '--skip-deps',
        '--no-deps',
        '-i',
        default=False,
        action='store_true',
        help='skips installation of dependencies',
    )
    parser.add_argument(
        '--no-install',
        default=False,
        action='store_true',
        help='do not install after building',
    )
    parser.add_argument(
        '--ytdl',
        default=False,
        action='store_true',
        help='version++ if new youtube-dl available',
    )
    parser.add_argument(
        '--ci',
        default=False,
        action='store_true',
        help='if running in a CI do not prompt just fail',
    )
    args = parser.parse_args()

    if args.clean:
        shutil.rmtree(DIST_DIR, True)
        shutil.rmtree('build', True)
        for file in glob.iglob('*.log'):
            os.remove(file)
        sys.exit()

    if args.deps:
        print('Installing Music Caster dependencies')
    else:
        print('Building Music Caster')

    if args.ytdl:
        upgrade_yt_dlp()
    else:
        update_versions(VERSION)
    print('Updated versions of build files')
    if args.ver_update:
        sys.exit()
    install_to_user = '' if IS_VENV else '--user'
    pip_cmd = f'"{sys.executable}" -m pip install --upgrade {install_to_user} --upgrade-strategy eager -r "{REQUIREMENTS_FILE}" -r "{REQUIREMENTS_DEV_FILE}"'
    if args.deps or (not args.skip_build and not args.skip_deps):
        print('Installing and/or upgrading dependencies...')
        if platform.system() == 'Windows':
            # install tkdnd custom way
            sys_dir_name = Path(sys.executable).parent
            if IS_VENV:
                shutil.copytree(
                    TKDND_FILES[0],
                    f'{sys_dir_name}/tcl/tkdnd2.9.2',
                    dirs_exist_ok=True,
                )
                shutil.copytree(
                    TKDND_FILES[1],
                    f'{sys_dir_name.parent}/Lib/site-packages/TkinterDnD2',
                    dirs_exist_ok=True,
                )
            else:
                shutil.copytree(TKDND_FILES[0],
                                f'{sys_dir_name}/tcl/tkdnd2.9.2',
                                dirs_exist_ok=True)
                shutil.copytree(
                    TKDND_FILES[1],
                    f'{sys_dir_name}/Lib/site-packages/TkinterDnD2',
                    dirs_exist_ok=True,
                )
        install_time_start = time.time()
        p = Popen(pip_cmd, stdin=DEVNULL, stdout=None if args.deps else DEVNULL, text=True)
        if p.wait() > 0:
            print(
                'ERROR: pip install failed\n',
                pip_cmd,
            )
            sys.exit(1)
        if args.deps:
            print(f'Dependencies installed ({time.time() - install_time_start:.0} seconds)')
            sys.exit()
    assert IS_VENV
    # import third party libraries
    import requests
    from git import Repo

    sys.argv = sys.argv[:1]
    from src.shared import get_running_processes, is_already_running

    args.upload = args.upload and not args.test_auto_update
    try:
        player_state = requests.get('http://[::1]:2001/state').json()
        requests.get('http://[::1]:2001/exit')
        time.sleep(1)  # wait for MC to exit
    except requests.exceptions.RequestException:
        player_state = {}
    for process in get_running_processes('Music Caster.exe'):
        # force close any other instances of MC
        pid = process['pid']
        with suppress(PermissionError):
            os.kill(pid, 9)
    if args.debug:
        set_spec_debug(True)
    else:
        set_spec_debug(False)
    if args.upload:
        print('Will upload to GitHub after building')
    if args.test_auto_update:
        print("This test should test auto update and won't publish to GitHub")

    if not args.skip_build:
        # remove existing builds
        try:
            with suppress(FileNotFoundError):
                shutil.rmtree('dist/Music Caster OneDir', False)
        except PermissionError:
            print('Files in "dist/Music Caster OneDir" are in use somehow')
            sys.exit()
        main_file = 'Music Caster'
        if platform.system() == 'Windows':
            main_file += '.exe'
        for dist_file in (main_file, f'{SETUP_OUTPUT_NAME}.exe',
                          'Portable.zip'):
            with suppress(FileNotFoundError):
                dist_file = DIST_DIR / dist_file
                print(f'Removing {dist_file}')
                os.remove(dist_file)

    if not args.skip_build:
        print(f'building executables with debug={args.debug}')
        additional_args = '--log=DEBUG' if args.debug else ''
        if args.clean:
            additional_args += ' --clean'

        if USING_TAURI_FRONTEND:
            # Only build daemon for Tauri frontend
            print('Building daemon for Tauri frontend...')
            check_call(
                f'{sys.executable} -O -m PyInstaller -y {additional_args} {DAEMON_SPEC}',
                shell=True,
            )
            s1 = None
            s4 = None
        else:
            # build frontend
            # check_call('yarn build', cwd=SRC_FRONTEND, shell=True)
            if platform.system() == 'Windows':
                s1 = Popen(
                    f'{sys.executable} -O -m PyInstaller -y {additional_args} {PORTABLE_SPEC}',
                    shell=True,
                )
            else:
                s1 = None
            try:
                # build Updater
                # install go dependencies
                check_call('go install github.com/akavel/rsrc@latest')
                check_call(
                    f'rsrc -manifest "{UPDATER_MANIFEST_FILE}" -ico "{UPDATER_ICO}"'
                )
                check_call(
                    f'go build -ldflags "-s -w -H windowsgui" -o "{UPDATER_DIST}"',
                    cwd=SRC_DIR)
            except Exception as e:
                if args.upload:
                    raise Exception('failed to build updater') from e
                print(f'WARNING: {e}')
            check_call(
                f'{sys.executable} -O -m PyInstaller -y {additional_args} {ONEDIR_SPEC}',
                shell=True,
            )
            try:
                if platform.system() == 'Windows':
                    s4 = Popen(f'iscc "{INSTALLER_SCRIPT}"')
                else:
                    s4 = None
            except FileNotFoundError:
                s4 = None
                print(
                    'WARNING: could not create an installer because iscc is not installed or is not on PATH'
                )

            try:
                portable_failed = s1.wait()
            except AttributeError:
                portable_failed = False
            if args.debug:
                set_spec_debug(False)
            if portable_failed:
                print('Portable installation failed')
                print(s1.communicate()[1])
                sys.exit()

        # Portable
        if platform.system() == 'Windows':
            music_caster_portable = (DIST_DIR / 'Music Caster.exe',
                                     'Music Caster.exe')
            updater_portable = (DIST_DIR / 'Updater.exe', 'Updater.exe')
            portable_files = [
                music_caster_portable,
                (CHANGELOG_FILE, 'CHANGELOG.txt'),
                updater_portable,
            ]
            vlc_ext = 'dll' if platform.system() == 'Windows' else 'so'
            print('Creating dist/Portable.zip')
            create_zip(
                DIST_DIR / 'Portable.zip',
                portable_files,
                compression=zipfile.ZIP_DEFLATED,
            )
        # zip directory for Linux or Darwin
        elif platform.system() == 'Darwin':
            pass
        else:
            shutil.rmtree(DIST_DIR / 'Music Caster OneDir/share/')
            linux_dist = DIST_DIR / 'Music Caster (Linux)'
            print(f'Creating {linux_dist}.zip')
            shutil.make_archive(linux_dist, 'zip', 'dist/Music Caster OneDir')
        with suppress(AttributeError):
            s4.wait()  # Wait for InnoSetup script to finish
        print(f'v{VERSION} Build Time:', round(time.time() - start_time, 2),
              'seconds')
        print('Last commit: ' + getoutput('git log --format="%H" -n 1'))

    if platform.system() == 'Windows':
        dist_files = ('Music Caster Setup.exe', 'Portable.zip')
    elif platform.system() == 'Darwin':
        dist_files = ('Music Caster (OSX).zip', )
    else:
        dist_files = ('Music Caster (Linux).zip', )

    # check if all files were built
    dist_files_exist = True
    for dist_file in dist_files:
        dist_file_path = DIST_DIR / dist_file
        if os.path.exists(dist_file_path):
            file_size = os.path.getsize(dist_file_path) // 1000  # KB
            file_exists_str = f'EXISTS {file_size:,} KB'.rjust(12)
        else:
            file_exists_str = 'DOES NOT EXIST!'
            dist_files_exist = False
        print((dist_file + ':').ljust(30) + file_exists_str)

    if dist_files_exist and platform.system() == 'Windows':
        with zipfile.ZipFile(DIST_DIR / 'Portable.zip') as portable_zip:
            if 'Updater.exe' in portable_zip.namelist():
                print('Portable.zip/Updater.exe:'.ljust(30) + 'EXISTS')
            else:
                print('Portable.zip/Updater.exe:'.ljust(30) +
                      'DOES NOT EXIST!')
                dist_files_exist = False

    daemon_dist = DIST_DIR / 'Music Caster Daemon.exe'
    if daemon_dist.exists() and USING_TAURI_FRONTEND:
        shutil.copy2(daemon_dist, DIST_DIR / 'music-caster-daemon-x86_64-pc-windows-msvc.exe')

    if not args.skip_tests and dist_files_exist and not USING_TAURI_FRONTEND:
        try:
            sys.argv = sys.argv[:1]
            pytest_args = ['pytest']
            if args.upload:
                pytest_args.append('--upload')
            if args.test_auto_update:
                pytest_args.append('--test-auto-update')
            if args.ci:
                pytest_args.append('--ci')
            pytest_args.append('--capture=no')
            check_call(pytest_args, cwd=SRC_DIR)
        except CalledProcessError:
            print('pytest: failed')
            sys.exit(1)
        # Test if executable can be run
        import appdirs
        user_data_dir = Path(appdirs.user_data_dir(roaming=True))
        test(
            'User data dir exists',
            lambda: user_data_dir.exists(),
            True,
        )
        p = Popen(
            f'"{DIST_DIR}/Music Caster OneDir/Music Caster" -m --debug',
            shell=True)
        time.sleep(5)
        p.poll()
        if p.returncode is not None:
            print('got return code', p.returncode)
        test(
            'No return code',
            lambda: p.returncode is None,
            True,
        )
        test(
            'Music Caster Should Be Running',
            lambda: is_already_running(threshold=1),
            True,
        )
        time.sleep(2)
        test('Music Caster Should Accept Exit API',
             lambda: requests.post('http://[::1]:2001/exit'))
        time.sleep(2)
        test('Music Caster Should Have Exited',
             lambda: not is_already_running(), True)

    if args.debug or not dist_files_exist:
        print(
            'Exiting early to avoid upload or installation of possibly broken build'
        )
        sys.exit(0 if args.dist_files_exist else 2)
    print(f'Build v{VERSION} complete')
    print('Time taken:', round(time.time() - start_time, 2), 'seconds')
    print('Last commit: ' + getoutput('git log --format="%H" -n 1'))
    if args.upload:
        print('Will try to upload to GitHub')
        # upload to GitHub
        github = read_env()['github']
        headers = {
            'Authorization': f'token {github}',
            'Accept': 'application/vnd.github.v3+json',
        }
        USERNAME = 'elibroftw'
        github_api = 'https://api.github.com'

        # check if tag vVERSION does not exist
        r = requests.get(
            f'{github_api}/repos/{USERNAME}/music-caster/releases/tags/v{VERSION}',
            headers=headers,
        )
        if r.status_code != 404:
            if args.ci:
                print('INFO: not uploading build since tag already exists')
                sys.exit(0)
            print(f'ERROR: Release for tag "v{VERSION}" already exists')
            sys.exit(1)

        old_release = requests.get(
            f'{github_api}/repos/{USERNAME}/music-caster/releases/latest'
        ).json()
        try:
            old_release_id = old_release['id']
        except KeyError:
            print(
                'rate limit exceeded, upload manually at https://github.com/elibroftw/music-caster/releases'
            )
            sys.exit()
        # keep changes of current major version if new version is a minor update
        body = '' if VERSION.endswith('.0') else old_release['body']
        body = add_new_changes(body)
        if any(Repo('.git').index.diff(None)):
            # possible if build.cmd -v wasn't run before
            print('Warning: Changes detected.')
            print(Repo('.git').index.diff(None))
            if not args.ci:
                input(
                    'Changed (not committed) files detected. Press enter to confirm upload.\n'
                )
        print('Will upload and install at the same time!')
        t = threading.Thread(target=local_install)
        t.start()

        new_release = {
            'tag_name': f'v{VERSION}',
            'target_commitish': 'master',
            'name': f'Music Caster v{VERSION}',
            'body': body,
            'draft': True,
            'prerelease': False,
        }
        r = requests.post(
            f'{github_api}/repos/{USERNAME}/music-caster/releases',
            json=new_release,
            headers=headers,
        )
        release = r.json()
        upload_url = release['upload_url'][:-13]
        release_id = release['id']
        # upload assets
        for dist_file in dist_files:
            requests.post(
                upload_url,
                data=ProgressUpload(DIST_DIR / dist_file),
                params={'name': dist_file},
                headers={
                    **headers, 'Content-Type': 'application/octet-stream'
                },
            )
        requests.post(
            f'{github_api}/repos/{USERNAME}/music-caster/releases/{release_id}',
            headers=headers,
            json={
                'body': body,
                'draft': False
            },
        )
        # since winget is slower on the PR's, it's better to not delete anything
        # if not VERSION.endswith('.0'):
        #     # delete old release if not a new major build
        #     requests.delete(f'{github_api}/repos/{USERNAME}/music-caster/releases/{old_release_id}', headers=headers)
        print(f'Published Release v{VERSION}')
        print(
            f'v{VERSION} Total Time Taken:',
            round(time.time() - start_time, 2),
            'seconds',
        )
        t.join()
    elif not args.no_install and (not args.skip_tests or args.force_install):
        print(
            'Installing Music Caster and it will be launched after installation.'
        )
        local_install()


================================================
FILE: build_files/TkinterDnD2/TkinterDnD.py
================================================
# -*- coding: utf-8 -*-

"""Python wrapper for the tkdnd tk extension.
The tkdnd extension provides an interface to native, platform specific
drag and drop mechanisms. Under Unix the drag & drop protocol in use is
the XDND protocol version 5 (also used by the Qt toolkit, and the KDE and
GNOME desktops). Under Windows, the OLE2 drag & drop interfaces are used.
Under Macintosh, the Cocoa drag and drop interfaces are used.

Once the TkinterDnD2 package is installed, it is safe to do:

from TkinterDnD2 import *

This will add the classes TkinterDnD.Tk and TkinterDnD.TixTk to the global
namespace, plus the following constants:
PRIVATE, NONE, ASK, COPY, MOVE, LINK, REFUSE_DROP,
DND_TEXT, DND_FILES, DND_ALL, CF_UNICODETEXT, CF_TEXT, CF_HDROP,
FileGroupDescriptor, FileGroupDescriptorW

Drag and drop for the application can then be enabled by using one of the
classes TkinterDnD.Tk() or (in case the tix extension shall be used)
TkinterDnD.TixTk() as application main window instead of a regular
tkinter.Tk() window. This will add the drag-and-drop specific methods to the
Tk window and all its descendants."""

try:
    import Tkinter as tkinter # type: ignore
    import tkinter.ttk as ttk # type: ignore
except ImportError:
    import tkinter
    import tkinter.ttk as ttk

TkdndVersion = None


def _require(tkroot):
    """Internal function."""
    global TkdndVersion
    try:
        TkdndVersion = tkroot.tk.call('package', 'require', 'tkdnd')
    except tkinter.TclError:
        raise RuntimeError('Unable to load tkdnd library.')
    return TkdndVersion


class DnDEvent:
    """Internal class.
    Container for the properties of a drag-and-drop event, similar to a
    normal tkinter.Event.
    An instance of the DnDEvent class has the following attributes:
        action (string)
        actions (tuple)
        button (int)
        code (string)
        codes (tuple)
        commonsourcetypes (tuple)
        commontargettypes (tuple)
        data (string)
        name (string)
        types (tuple)
        modifiers (tuple)
        supportedsourcetypes (tuple)
        sourcetypes (tuple)
        type (string)
        supportedtargettypes (tuple)
        widget (widget instance)
        x_root (int)
        y_root (int)
    Depending on the type of DnD event however, not all attributes may be set.
    """
    pass


class DnDWrapper:
    """Internal class."""
    # some of the percent substitutions need to be enclosed in braces
    # so we can use splitlist() to convert them into tuples
    _subst_format_dnd = ('%A', '%a', '%b', '%C', '%c', '{%CST}',
                         '{%CTT}', '%D', '%e', '{%L}', '{%m}', '{%ST}',
                         '%T', '{%t}', '{%TT}', '%W', '%X', '%Y')
    _subst_format_str_dnd = " ".join(_subst_format_dnd)
    tkinter.BaseWidget._subst_format_dnd = _subst_format_dnd
    tkinter.BaseWidget._subst_format_str_dnd = _subst_format_str_dnd

    def _substitute_dnd(self, *args):
        """Internal function."""
        if len(args) != len(self._subst_format_dnd):
            return args

        def getint_event(s):
            try:
                return int(s)
            except ValueError:
                return s

        def splitlist_event(s):
            try:
                return self.tk.splitlist(s)
            except ValueError:
                return s

        # valid percent substitutions for DnD event types
        # (tested with tkdnd-2.8 on debian jessie):
        # <<DragInitCmd>> : %W, %X, %Y %e, %t
        # <<DragEndCmd>> : %A, %W, %e
        # <<DropEnter>> : all except : %D (always empty)
        # <<DropLeave>> : all except %D (always empty)
        # <<DropPosition>> :all except %D (always empty)
        # <<Drop>> : all
        A, a, b, C, c, CST, CTT, D, e, L, m, ST, T, t, TT, W, X, Y = args
        ev = DnDEvent()
        ev.action = A
        ev.actions = splitlist_event(a)
        ev.button = getint_event(b)
        ev.code = C
        ev.codes = splitlist_event(c)
        ev.commonsourcetypes = splitlist_event(CST)
        ev.commontargettypes = splitlist_event(CTT)
        ev.data = D
        ev.name = e
        ev.types = splitlist_event(L)
        ev.modifiers = splitlist_event(m)
        ev.supportedsourcetypes = splitlist_event(ST)
        ev.sourcetypes = splitlist_event(t)
        ev.type = T
        ev.supportedtargettypes = splitlist_event(TT)
        try:
            ev.widget = self.nametowidget(W)
        except KeyError:
            ev.widget = W
        ev.x_root = getint_event(X)
        ev.y_root = getint_event(Y)
        return ev,

    tkinter.BaseWidget._substitute_dnd = _substitute_dnd

    def _dnd_bind(self, what, sequence, func, add, needcleanup=True):
        """Internal function."""
        if isinstance(func, str):
            self.tk.call(what + (sequence, func))
        elif func:
            funcid = self._register(func, self._substitute_dnd, needcleanup)
            # FIXME: why doesn't the "return 'break'" mechanism work here??
            # cmd = ('%sif {"[%s %s]" == "break"} break\n' % (add and '+' or '',
            #                              funcid, self._subst_format_str_dnd))
            cmd = '%s%s %s' % (add and '+' or '', funcid,
                               self._subst_format_str_dnd)
            self.tk.call(what + (sequence, cmd))
            return funcid
        elif sequence:
            return self.tk.call(what + (sequence,))
        else:
            return self.tk.splitlist(self.tk.call(what))

    tkinter.BaseWidget._dnd_bind = _dnd_bind

    def dnd_bind(self, sequence=None, func=None, add=None):
        """Bind to this widget at drag and drop event SEQUENCE a call
        to function FUNC.
        SEQUENCE may be one of the following:
        <<DropEnter>>, <<DropPosition>>, <<DropLeave>>, <<Drop>>,
        <<Drop:type>>, <<DragInitCmd>>, <<DragEndCmd>> .
        The callbacks for the <Drop*>> events, with the exception of
        <<DropLeave>>, should always return an action (i.e. one of COPY,
        MOVE, LINK, ASK or PRIVATE).
        The callback for the <<DragInitCmd>> event must return a tuple
        containing three elements: the drop action(s) supported by the
        drag source, the format type(s) that the data can be dropped as and
        finally the data that shall be dropped. Each of these three elements
        may be a tuple of strings or a single string."""
        return self._dnd_bind(('bind', self._w), sequence, func, add)

    tkinter.BaseWidget.dnd_bind = dnd_bind

    def drag_source_register(self, button=None, *dndtypes):
        """This command will register SELF as a drag source.
        A drag source is a widget than can start a drag action. This command
        can be executed multiple times on a widget.
        When SELF is registered as a drag source, optional DNDTYPES can be
        provided. These DNDTYPES will be provided during a drag action, and
        it can contain platform independent or platform specific types.
        Platform independent are DND_Text for dropping text portions and
        DND_Files for dropping a list of files (which can contain one or
        multiple files) on SELF. However, these types are
        indicative/informative. SELF can initiate a drag action with even a
        different type list. Finally, button is the mouse button that will be
        used for starting the drag action. It can have any of the values 1
        (left mouse button), 2 (middle mouse button - wheel) and 3
        (right mouse button). If button is not specified, it defaults to 1."""
        # hack to fix a design bug from the first version
        if button is None:
            button = 1
        else:
            try:
                button = int(button)
            except ValueError:
                # no button defined, button is actually
                # something like DND_TEXT
                dndtypes = (button,) + dndtypes
                button = 1
        self.tk.call(
            'tkdnd::drag_source', 'register', self._w, dndtypes, button)

    tkinter.BaseWidget.drag_source_register = drag_source_register

    def drag_source_unregister(self):
        """This command will stop SELF from being a drag source. Thus, window
        will stop receiving events related to drag operations. It is an error
        to use this command for a window that has not been registered as a
        drag source with drag_source_register()."""
        self.tk.call('tkdnd::drag_source', 'unregister', self._w)

    tkinter.BaseWidget.drag_source_unregister = drag_source_unregister

    def drop_target_register(self, *dndtypes):
        """This command will register SELF as a drop target. A drop target is
        a widget than can accept a drop action. This command can be executed
        multiple times on a widget. When SELF is registered as a drop target,
        optional DNDTYPES can be provided. These types list can contain one or
        more types that SELF will accept during a drop action, and it can
        contain platform independent or platform specific types. Platform
        independent are DND_Text for dropping text portions and DND_Files for
        dropping a list of files (which can contain one or multiple files) on
        SELF."""
        self.tk.call('tkdnd::drop_target', 'register', self._w, dndtypes)

    tkinter.BaseWidget.drop_target_register = drop_target_register

    def drop_target_unregister(self):
        """This command will stop SELF from being a drop target. Thus, SELF
        will stop receiving events related to drop operations. It is an error
        to use this command for a window that has not been registered as a
        drop target with drop_target_register()."""
        self.tk.call('tkdnd::drop_target', 'unregister', self._w)

    tkinter.BaseWidget.drop_target_unregister = drop_target_unregister

    def platform_independent_types(self, *dndtypes):
        """This command will accept a list of types that can contain platform
        independent or platform specific types. A new list will be returned,
        where each platform specific type in DNDTYPES will be substituted by
        one or more platform independent types. Thus, the returned list may
        have more elements than DNDTYPES."""
        return self.tk.split(self.tk.call(
            'tkdnd::platform_independent_types', dndtypes))

    tkinter.BaseWidget.platform_independent_types = platform_independent_types

    def platform_specific_types(self, *dndtypes):
        """This command will accept a list of types that can contain platform
        independent or platform specific types. A new list will be returned,
        where each platform independent type in DNDTYPES will be substituted
        by one or more platform specific types. Thus, the returned list may
        have more elements than DNDTYPES."""
        return self.tk.split(self.tk.call(
            'tkdnd::platform_specific_types', dndtypes))

    tkinter.BaseWidget.platform_specific_types = platform_specific_types

    def get_dropfile_tempdir(self):
        """This command will return the temporary directory used by TkDND for
        storing temporary files. When the package is loaded, this temporary
        directory will be initialised to a proper directory according to the
        operating system. This default initial value can be changed to be the
        value of the following environmental variables:
        TKDND_TEMP_DIR, TEMP, TMP."""
        return self.tk.call('tkdnd::GetDropFileTempDirectory')

    tkinter.BaseWidget.get_dropfile_tempdir = get_dropfile_tempdir

    def set_dropfile_tempdir(self, tempdir):
        """This command will change the temporary directory used by TkDND for
        storing temporary files to TEMPDIR."""
        self.tk.call('tkdnd::SetDropFileTempDirectory', tempdir)

    tkinter.BaseWidget.set_dropfile_tempdir = set_dropfile_tempdir


#      The main window classes that enable Drag & Drop for
#      themselves and all their descendant widgets:
class Tk(tkinter.Tk, DnDWrapper):
    """Creates a new instance of a tkinter.Tk() window; all methods of the
    DnDWrapper class apply to this window and all its descendants."""

    def __init__(self, *args, **kw):
        tkinter.Tk.__init__(self, *args, **kw)
        self.TkdndVersion = _require(self)


class TixTk(tkinter.Tk, DnDWrapper):
    """Creates a new instance of a tkinter.Tk() window with ttk support; all methods of the
    DnDWrapper class apply to this window and all its descendants."""

    def __init__(self, *args, **kw):
        tkinter.Tk.__init__(self, *args, **kw)
        self.TkdndVersion = _require(self)


================================================
FILE: build_files/TkinterDnD2/__init__.py
================================================
# dnd actions
PRIVATE = 'private'
NONE = 'none'
ASK = 'ask'
COPY = 'copy'
MOVE = 'move'
LINK = 'link'
REFUSE_DROP = 'refuse_drop'
# dnd types
DND_TEXT = 'DND_Text'
DND_FILES = 'DND_Files'
DND_ALL = '*'
CF_UNICODETEXT = 'CF_UNICODETEXT'
CF_TEXT = 'CF_TEXT'
CF_HDROP = 'CF_HDROP'
FileGroupDescriptor = 'FileGroupDescriptor - FileContents'# ??
FileGroupDescriptorW = 'FileGroupDescriptorW - FileContents'# ??

from TkinterDnD2 import TkinterDnD


================================================
FILE: build_files/Updater.cs.txt
================================================
// NOTE: This was the old portable updater
//  the new updater is updater.go
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Text.Json;

/**
old code in build.py that was used to build the updater
def get_msbuild():
    import re
    import winreg
    reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
    root_key = winreg.OpenKey(reg, r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
                              0, winreg.KEY_READ | winreg.KEY_WOW64_32KEY)
    num_sub_keys = winreg.QueryInfoKey(root_key)[0]
    vs = {}
    for i in range(num_sub_keys):
        with suppress(EnvironmentError):
            software: dict = {}
            software_key = winreg.EnumKey(root_key, i)
            software_key = winreg.OpenKey(root_key, software_key)
            info_key = winreg.QueryInfoKey(software_key)
            for value in range(info_key[1]):
                value = winreg.EnumValue(software_key, value)
                software[value[0]] = value[1]
            display_name = software.get('DisplayName', '')
            if re.search(r'Visual Studio (Community|Professional|Enterprise)', display_name):
                software['ver'] = int(software['DisplayName'].rsplit(maxsplit=1)[1])
                vs_ver = vs.get('ver', 0)
                if software['ver'] > vs_ver:
                    vs = software
    if vs is None: raise RuntimeWarning('No installation of Visual Studio could be found')
    ms_build_path = vs['InstallLocation'] + r'\MSBuild\Current\Bin\MSBuild.exe'
    return ms_build_path

...
ms_build = get_msbuild()
check_call(f'{ms_build} "{starting_dir}/Music Caster Updater/Music Caster Updater.sln"'
           f' /t:Build /p:Configuration=Release /p:PlatformTarget=x86')
...
# portable_files.extend([(f, os.path.basename(f)) for f in glob.iglob(f'{glob.escape(UPDATER_DIST_PATH)}/*.*')])
*/

namespace Music_Caster_Updater
{
    class Program
    {
        private static void ExtractZip(string fileName)
        {
            /**
             * Extracts fileName (ends with .zip) to root directory
             * Deletes fileName after
             */
            using (ZipArchive archive = ZipFile.OpenRead(fileName))
            {
                foreach (ZipArchiveEntry entry in archive.Entries)
                {
                    string dir = Path.GetDirectoryName(entry.FullName);
                    if (dir != "" && !Directory.Exists(dir)) Directory.CreateDirectory(dir);
                    try
                    {
                        if (File.Exists(entry.FullName)) File.Delete(entry.FullName);
                        entry.ExtractToFile(entry.FullName);
                    }
                    catch (IOException) { }
                    catch (System.UnauthorizedAccessException) { }
                }
            }
            File.Delete(fileName);
        }
        private static void Download(string url, string outfile)
        {
            // Downloads url to outfile
            // If outfile is a zip, extract it
            Debug.WriteLine($"Downloading {outfile}");
            using WebClient myWebClient = new WebClient();
            myWebClient.DownloadFile(url, outfile);
            if (outfile.EndsWith(".zip")) ExtractZip(outfile);
        }

        private static List<string> DirectorySearch(string dir)
        {   // returns all files in a dir and its subdirs recursively
            List<string> files = new List<string>();
            try
            {
                foreach (string f in Directory.GetFiles(dir)) files.Add(Path.GetFileName(f));
                foreach (string d in Directory.GetDirectories(dir)) files.AddRange(DirectorySearch(d));
            }
            catch (Exception) { }
            return files;
        }


        static void Main()
        {
            // use @ for string literals
            const string releasesURL = @"https://api.github.com/repos/elibroftw/music-caster/releases/latest";
            const string settingsFile = "settings.json";
            Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);  // Change working dir to dir of this program
            Dictionary<string, object> loadedSettings = new Dictionary<string, object>() { { "DEBUG", false } };

            if (File.Exists(settingsFile))
            {
                using StreamReader fs = new StreamReader(settingsFile);
                loadedSettings = JsonSerializer.Deserialize<Dictionary<string, object>>(fs.ReadToEnd());
            }
            bool debugSetting = false;
            try
            {
                debugSetting = ((JsonElement)loadedSettings.GetValueOrDefault("DEBUG")).GetBoolean();
            }
            catch (InvalidCastException) { }


            Dictionary<string, object> jsonResponse;

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(releasesURL);
            request.Method = "GET";
            request.UserAgent = "MusicCasterUpdaterC#";
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            using (Stream stream = response.GetResponseStream())
            using (StreamReader reader = new StreamReader(stream))
            {
                jsonResponse = JsonSerializer.Deserialize<Dictionary<string, object>>((new StreamReader(response.GetResponseStream())).ReadToEnd());
            }

            string setupDownloadURL = "", portableDownloadURL = "";

            JsonElement assets = (JsonElement) jsonResponse.GetValueOrDefault("assets");
            foreach (JsonElement asset in assets.EnumerateArray())
            {
                if (asset.GetProperty("name").ToString().Contains("exe"))
                    setupDownloadURL = asset.GetProperty("browser_download_url").ToString();
                else if (asset.GetProperty("name").ToString().Tocasefold().Contains("portable"))
                    portableDownloadURL = asset.GetProperty("browser_download_url").ToString();
            }
            if (debugSetting)
            {
                string latestVersion = jsonResponse.GetValueOrDefault("tag_name").ToString();
                Debug.WriteLine($"Latest Version: {latestVersion}");
                Debug.WriteLine($"Portable:       {portableDownloadURL}");
                Debug.WriteLine($"Installer:      {setupDownloadURL}");
            }
            else if (File.Exists("unins000.exe"))
            {   // Was installed using the Installer
                Download(setupDownloadURL, "MC_Installer.exe");
                Process.Start("MC_Installer.exe", "/VERYSILENT /CLOSEAPPLICATIONS /FORCECLOSEAPPLICATIONS /MERGETASKS=\"!desktopicon\"");
            }
            else
            {   // portable installation
                Download(portableDownloadURL, "Portable.zip");
                Process.Start("\"Music Caster.exe\" --nupdate");
            }
        }
    }
}


================================================
FILE: build_files/Updater.exe.MANIFEST
================================================
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity type="win32" name="updater" processorArchitecture="x86" version="1.0.0.0"/>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" language="*" processorArchitecture="*" version="6.0.0.0" publicKeyToken="6595b64144ccf1df"/>
      <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"/>
    </dependentAssembly>
  </dependency>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!-- Windows Vista -->
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
      <!-- Windows 7 -->
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
      <!-- Windows 8 -->
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
      <!-- Windows 8.1 -->
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
      <!-- Windows 10 -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
    </application>
  </compatibility>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
        <security>
            <requestedPrivileges>
                <!--
                  UAC settings:
                  - app should run at same integrity level as calling process
                  - app does not need to manipulate windows belonging to higher-integrity-level processes
                  -->
                <requestedExecutionLevel level="asInvoker" uiAccess="false"
                />
            </requestedPrivileges>
        </security>
    </trustInfo>
</assembly>

================================================
FILE: build_files/daemon.spec
================================================
# -*- mode: python ; coding: utf-8 -*-
import os
from PyInstaller.building.api import PYZ, EXE
from PyInstaller.building.build_main import Analysis, Tree # type: ignore
from PyInstaller.config import CONF
import platform

CONF['distpath'] = './dist' # type: ignore
# CONF['workpath'] = './build'
block_cipher = None
a = Analysis([f'{os.getcwd()}/src/music_caster.py'],
             pathex=[os.getcwd()],
             binaries=[],
             datas=[('../CHANGELOG.TXT', '.')],
             hiddenimports=['pystray._win32', 'zeroconf._utils.ipaddress', 'zeroconf._handlers.answers'],
             hookspath=[],
             runtime_hooks=[],
             excludes=['crypto', 'cryptography', 'pycryptodome', 'pandas', 'gevent',
                       'numpy', 'simplejson', 'PySide2', 'PyQt5', 'greenlet'],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

a.datas.extend(Tree('src/templates', 'templates'))
a.datas.extend(Tree('src/static', 'static'))
VLC_EXCLUDES = ['*.dll', '*.so', '*.so*', '*.dylib*', '*.dylib']
if platform.system() == 'Windows':
    VLC_EXCLUDES.remove('*.dll')
elif platform.system() == 'Darwin':
    VLC_EXCLUDES.remove('*.dylib*')
    VLC_EXCLUDES.remove('*.dylib')
elif platform.system() == 'Linux':
    VLC_EXCLUDES.remove('*.so*')
    VLC_EXCLUDES.remove('*.so')
a.datas.extend(Tree('src/vlc_lib', 'vlc_lib', excludes=VLC_EXCLUDES))
a.datas.extend(Tree('src/languages', 'languages'))
a.datas.extend(Tree('build_files/tkdnd2.9.2', 'tkdnd2.9.2'))
a.datas.extend(Tree('src/theme', 'theme'))
# a.datas.extend(Tree('src-frontend/dist', 'frontend'))

pyz = PYZ(a.pure, a.zipped_data,
          cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='Music Caster Daemon',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=False,
          runtime_tmpdir=None,
          # TODO: use ENV variable
          console=True, version='mc_version_info.txt', icon=os.path.abspath('resources/Music Caster Icon.ico'))


================================================
FILE: build_files/flatpak-pip-generator.py
================================================
#!/usr/bin/env python3

__license__ = 'MIT'

import argparse
import json
import hashlib
import os
import shutil
import subprocess
import sys
import tempfile
import urllib.request

from collections import OrderedDict
from typing import Dict

import requirements

parser = argparse.ArgumentParser()
parser.add_argument('packages', nargs='*')
parser.add_argument('--python2', action='store_true',
                    help='Look for a Python 2 package')
parser.add_argument('--cleanup', choices=['scripts', 'all'],
                    help='Select what to clean up after build')
parser.add_argument('--requirements-file', '-r',
                    help='Specify requirements.txt file')
parser.add_argument('--build-only', action='store_const',
                    dest='cleanup', const='all',
                    help='Clean up all files after build')
parser.add_argument('--build-isolation', action='store_true',
                    default=False,
                    help=(
                        'Do not disable build isolation. '
                        'Mostly useful on pip that does\'t '
                        'support the feature.'
                    ))
parser.add_argument('--checker-data', action='store_true',
                    help='Include x-checker-data in output for the "Flatpak External Data Checker"')
parser.add_argument('--output', '-o',
                    help='Specify output file name')
parser.add_argument('--runtime',
                    help='Specify a flatpak to run pip inside of a sandbox, ensures python version compatibility')
parser.add_argument('--yaml', action='store_true',
                    help='Use YAML as output format instead of JSON')
opts = parser.parse_args()

if opts.yaml:
    try:
        import yaml
    except ImportError:
        exit('PyYAML modules is not installed. Run "pip install PyYAML"')


def get_pypi_url(name: str, filename: str) -> str:
    url = 'https://pypi.org/pypi/{}/json'.format(name)
    print('Extracting download url for', name)
    with urllib.request.urlopen(url) as response:
        body = json.loads(response.read().decode('utf-8'))
        for release in body['releases'].values():
            for source in release:
                if source['filename'] == filename:
                    return source['url']
        raise Exception('Failed to extract url from {}'.format(url))


def get_tar_package_url_pypi(name: str, version: str) -> str:
    url = 'https://pypi.org/pypi/{}/{}/json'.format(name, version)
    with urllib.request.urlopen(url) as response:
        body = json.loads(response.read().decode('utf-8'))
        for ext in ['bz2', 'gz', 'xz', 'zip']:
            for source in body['urls']:
                if source['url'].endswith(ext):
                    return source['url']
        err = 'Failed to get {}-{} source from {}'.format(name, version, url)
        raise Exception(err)


def get_package_name(filename: str) -> str:
    if filename.endswith(('bz2', 'gz', 'xz', 'zip')):
        segments = filename.split('-')
        if len(segments) == 2:
            return segments[0]
        return '-'.join(segments[:len(segments) - 1])
    elif filename.endswith('whl'):
        segments = filename.split('-')
        if len(segments) == 5:
            return segments[0]
        candidate = segments[:len(segments) - 4]
        # Some packages list the version number twice
        # e.g. PyQt5-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux2014_x86_64.whl
        if candidate[-1] == segments[len(segments) - 4]:
            return '-'.join(candidate[:-1])
        return '-'.join(candidate)
    else:
        raise Exception(
            'Downloaded filename: {} does not end with bz2, gz, xz, zip, or whl'.format(filename)
        )


def get_file_version(filename: str) -> str:
    name = get_package_name(filename)
    segments = filename.split(name + '-')
    version = segments[1].split('-')[0]
    for ext in ['tar.gz', 'whl', 'tar.xz', 'tar.gz', 'tar.bz2', 'zip']:
        version = version.replace('.' + ext, '')
    return version


def get_file_hash(filename: str) -> str:
    sha = hashlib.sha256()
    print('Generating hash for', filename.split('/')[-1])
    with open(filename, 'rb') as f:
        while True:
            data = f.read(1024 * 1024 * 32)
            if not data:
                break
            sha.update(data)
        return sha.hexdigest()


def download_tar_pypi(url: str, tempdir: str) -> None:
    with urllib.request.urlopen(url) as response:
        file_path = os.path.join(tempdir, url.split('/')[-1])
        with open(file_path, 'x+b') as tar_file:
            shutil.copyfileobj(response, tar_file)


def parse_continuation_lines(fin):
    for line in fin:
        line = line.rstrip('\n')
        while line.endswith('\\'):
            try:
                line = line[:-1] + next(fin).rstrip('\n')
            except StopIteration:
                exit('Requirements have a wrong number of line continuation characters "\\"')
        yield line


def fprint(string: str) -> None:
    separator = '=' * 72  # Same as `flatpak-builder`
    print(separator)
    print(string)
    print(separator)


packages = []
if opts.requirements_file:
    requirements_file = os.path.expanduser(opts.requirements_file)
    try:
        with open(requirements_file, 'r') as req_file:
            reqs = parse_continuation_lines(req_file)
            reqs_as_str = '\n'.join([r.split('--hash')[0] for r in reqs])
            packages = list(requirements.parse(reqs_as_str))
    except FileNotFoundError:
        pass

elif opts.packages:
    packages = list(requirements.parse('\n'.join(opts.packages)))
    with tempfile.NamedTemporaryFile('w', delete=False, prefix='requirements.') as req_file:
        req_file.write('\n'.join(opts.packages))
        requirements_file = req_file.name
else:
    exit('Please specifiy either packages or requirements file argument')

for i in packages:
    if i["name"].casefold().startswith("pyqt"):
        print("PyQt packages are not supported by flapak-pip-generator")
        print("However, there is a BaseApp for PyQt available, that you should use")
        print("Visit https://github.com/flathub/com.riverbankcomputing.PyQt.BaseApp for more information")
        sys.exit(0)

with open(requirements_file, 'r') as req_file:
    use_hash = '--hash=' in req_file.read()

python_version = '2' if opts.python2 else '3'
if opts.python2:
    pip_executable = 'pip2'
else:
    pip_executable = 'pip3'

if opts.runtime:
    flatpak_cmd = [
        'flatpak',
        '--devel',
        '--share=network',
        '--filesystem=/tmp',
        '--command={}'.format(pip_executable),
        'run',
        opts.runtime
    ]
    if opts.requirements_file:
        requirements_file = os.path.expanduser(opts.requirements_file)
        if os.path.exists(requirements_file):
            prefix = os.path.realpath(requirements_file)
            flag = '--filesystem={}'.format(prefix)
            flatpak_cmd.insert(1,flag)
else:
    flatpak_cmd = [pip_executable]

if opts.output:
    output_package = opts.output
elif opts.requirements_file:
    output_package = 'python{}-{}'.format(
        python_version,
        os.path.basename(opts.requirements_file).replace('.txt', ''),
    )
elif len(packages) == 1:
    output_package = 'python{}-{}'.format(
        python_version, packages[0].name,
    )
else:
    output_package = 'python{}-modules'.format(python_version)
if opts.yaml:
    output_filename = output_package + '.yaml'
else:
    output_filename = output_package + '.json'

modules = []
vcs_modules = []
sources = {}

tempdir_prefix = 'pip-generator-{}'.format(os.path.basename(output_package))
with tempfile.TemporaryDirectory(prefix=tempdir_prefix) as tempdir:
    pip_download = flatpak_cmd + [
        'download',
        '--exists-action=i',
        '--dest',
        tempdir,
        '-r',
        requirements_file
    ]
    if use_hash:
        pip_download.append('--require-hashes')

    fprint('Downloading sources')
    cmd = ' '.join(pip_download)
    print('Running: "{}"'.format(cmd))
    try:
        subprocess.run(pip_download, check=True)
    except subprocess.CalledProcessError:
        print('Failed to download')
        print('Please fix the module manually in the generated file')

    if not opts.requirements_file:
        try:
            os.remove(requirements_file)
        except FileNotFoundError:
            pass

    fprint('Downloading arch independent packages')
    for filename in os.listdir(tempdir):
        if not filename.endswith(('bz2', 'any.whl', 'gz', 'xz', 'zip')):
            version = get_file_version(filename)
            name = get_package_name(filename)
            url = get_tar_package_url_pypi(name, version)
            print('Deleting', filename)
            try:
                os.remove(os.path.join(tempdir, filename))
            except FileNotFoundError:
                pass
            print('Downloading {}'.format(url))
            download_tar_pypi(url, tempdir)

    files = {get_package_name(f): [] for f in os.listdir(tempdir)}

    for filename in os.listdir(tempdir):
        name = get_package_name(filename)
        files[name].append(filename)

    # Delete redundant sources, for vcs sources
    for name in files:
        if len(files[name]) > 1:
            zip_source = False
            for f in files[name]:
                if f.endswith('.zip'):
                    zip_source = True
            if zip_source:
                for f in files[name]:
                    if not f.endswith('.zip'):
                        try:
                            os.remove(os.path.join(tempdir, f))
                        except FileNotFoundError:
                            pass

    vcs_packages = {
        x.name: {'vcs': x.vcs, 'revision': x.revision, 'uri': x.uri}
        for x in packages
        if x.vcs
    }

    fprint('Obtaining hashes and urls')
    for filename in os.listdir(tempdir):
        name = get_package_name(filename)
        sha256 = get_file_hash(os.path.join(tempdir, filename))

        if name in vcs_packages:
            uri = vcs_packages[name]['uri']
            revision = vcs_packages[name]['revision']
            vcs = vcs_packages[name]['vcs']
            url = 'https://' + uri.split('://', 1)[1]
            s = 'commit'
            if vcs == 'svn':
                s = 'revision'
            source = OrderedDict([
                ('type', vcs),
                ('url', url),
                (s, revision),
            ])
            is_vcs = True
        else:
            url = get_pypi_url(name, filename)
            source = OrderedDict([
                ('type', 'file'),
                ('url', url),
                ('sha256', sha256)])
            if opts.checker_data:
                source['x-checker-data'] = {
                    'type': 'pypi',
                    'name': name}
                if url.endswith(".whl"):
                    source['x-checker-data']['packagetype'] = 'bdist_wheel'
            is_vcs = False
        sources[name] = {'source': source, 'vcs': is_vcs}

# Python3 packages that come as part of org.freedesktop.Sdk.
system_packages = ['cython', 'easy_install', 'mako', 'markdown', 'meson', 'pip', 'pygments', 'setuptools', 'six', 'wheel']

fprint('Generating dependencies')
for package in packages:

    if package.name is None:
        print('Warning: skipping invalid requirement specification {} because it is missing a name'.format(package.line), file=sys.stderr)
        print('Append #egg=<pkgname> to the end of the requirement line to fix', file=sys.stderr)
        continue
    elif package.name.casefold() in system_packages:
        continue

    if len(package.extras) > 0:
        extras = '[' + ','.join(extra for extra in package.extras) + ']'
    else:
        extras = ''

    version_list = [x[0] + x[1] for x in package.specs]
    version = ','.join(version_list)

    if package.vcs:
        revision = ''
        if package.revision:
            revision = '@' + package.revision
        pkg = package.uri + revision + '#egg=' + package.name
    else:
        pkg = package.name + extras + version

    dependencies = []
    # Downloads the package again to list dependencies

    tempdir_prefix = 'pip-generator-{}'.format(package.name)
    with tempfile.TemporaryDirectory(prefix='{}-{}'.format(tempdir_prefix, package.name)) as tempdir:
        pip_download = flatpak_cmd + [
            'download',
            '--exists-action=i',
            '--dest',
            tempdir,
        ]
        try:
            print('Generating dependencies for {}'.format(package.name))
            subprocess.run(pip_download + [pkg], check=True, stdout=subprocess.DEVNULL)
            for filename in os.listdir(tempdir):
                dep_name = get_package_name(filename)
                if dep_name.casefold() in system_packages:
                    continue
                dependencies.append(dep_name)

        except subprocess.CalledProcessError:
            print('Failed to download {}'.format(package.name))

    is_vcs = True if package.vcs else False
    package_sources = []
    for dependency in dependencies:
        if dependency in sources:
            source = sources[dependency]
        elif dependency.replace('_', '-') in sources:
            source = sources[dependency.replace('_', '-')]
        else:
            continue

        if not (not source['vcs'] or is_vcs):
            continue

        package_sources.append(source['source'])

    if package.vcs:
        name_for_pip = '.'
    else:
        name_for_pip = pkg

    module_name = 'python{}-{}'.format(python_version, package.name)

    pip_command = [
        pip_executable,
        'install',
        '--verbose',
        '--exists-action=i',
        '--no-index',
        '--find-links="file://${PWD}"',
        '--prefix=${FLATPAK_DEST}',
        '"{}"'.format(name_for_pip)
    ]
    if not opts.build_isolation:
        pip_command.append('--no-build-isolation')

    module = OrderedDict([
        ('name', module_name),
        ('buildsystem', 'simple'),
        ('build-commands', [' '.join(pip_command)]),
        ('sources', package_sources),
    ])
    if opts.cleanup == 'all':
        module['cleanup'] = ['*']
    elif opts.cleanup == 'scripts':
        module['cleanup'] = ['/bin', '/share/man/man1']

    if package.vcs:
        vcs_modules.append(module)
    else:
        modules.append(module)

modules = vcs_modules + modules
if len(modules) == 1:
    pypi_module = modules[0]
else:
    pypi_module = {
        'name': output_package,
        'buildsystem': 'simple',
        'build-commands': [],
        'modules': modules,
    }

print()
with open(output_filename, 'w') as output:
    if opts.yaml:
        class OrderedDumper(yaml.Dumper):
            def increase_indent(self, flow=False, indentless=False):
                return super(OrderedDumper, self).increase_indent(flow, False)

        def dict_representer(dumper, data):
            return dumper.represent_dict(data.items())

        OrderedDumper.add_representer(OrderedDict, dict_representer)

        yaml.dump(pypi_module, output, Dumper=OrderedDumper)
    else:
        output.write(json.dumps(pypi_module, indent=4))
    print('Output saved to {}'.format(output_filename))


================================================
FILE: build_files/mc_version_info.txt
================================================
# UTF-8
# For more details about fixed file info 'ffi' see: http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
  ffi=FixedFileInfo(
    prodvers=(5, 25, 1, 0),
    filevers=(5, 25, 1, 0),
    # Contains a bitmask that specifies the valid bits 'flags'r
    mask=0x17,
    # Contains a bitmask that specifies the Boolean attributes of the file.
    flags=0x0,
    # The operating system for which this file was designed.
    # 0x4 - NT and there is no need to change it.
    OS=0x4,
    # The general type of file.
    # 0x1 - the file is an application.
    fileType=0x1,
    # The function of the file.
    # 0x0 - the function is not defined for this fileType
    subtype=0x0,
    # Creation date and time stamp.
    date=(0, 0)
    ),
  kids=[
    StringFileInfo(
      [
      StringTable(
        '000004b0',
        [StringStruct('CompanyName', 'Elijah Lopez'),
        StringStruct('FileDescription', 'Music Caster'),
        StringStruct('FileVersion', '5.25.1.0'),
        StringStruct('InternalName', 'Music Caster'),
        StringStruct('LegalCopyright', 'Copyright (c) 2019 - 2025, Elijah Lopez'),
        StringStruct('OriginalFilename', 'Music Caster.exe'),
        StringStruct('ProductName', 'Music Caster'),
        StringStruct('ProductVersion', '5.25.1.0')])
      ]),
    VarFileInfo([VarStruct('Translation', [0, 1200])])
  ]
)


================================================
FILE: build_files/mcu_version_info.txt
================================================
# UTF-8
VSVersionInfo(
    ffi=FixedFileInfo(
        prodvers=(2, 3, 0, 0),
        filevers=(2, 3, 0, 0),
        mask=0x17,
        flags=0x0,
        OS=0x4,
        fileType=0x1,
        subtype=0x0,
        date=(0, 0)),
    kids=[StringFileInfo(
        [StringTable(
            '000004b0',
            [StringStruct('CompanyName', 'Elijah Lopez'),
            StringStruct('FileDescription', 'Updater for Music Caster'),
            StringStruct('FileVersion', '2.3.0.0'),
            StringStruct('InternalName', 'Music Caster Updater'),
            StringStruct('LegalCopyright', 'Copyright (c) 2019 - 2020, Elijah Lopez'),
            StringStruct('OriginalFilename', 'Updater.exe'),
            StringStruct('ProductName', 'Music Caster Updater'),
            StringStruct('ProductVersion', '2.3.0.0')])]),
        VarFileInfo([VarStruct('Translation', [0, 1200])])])


================================================
FILE: build_files/onedir.spec
================================================
# -*- mode: python ; coding: utf-8 -*-
import os
from PyInstaller.building.api import PYZ, EXE, COLLECT
from PyInstaller.building.build_main import Analysis, Tree # type: ignore
from PyInstaller.config import CONF
import platform

CONF['distpath'] = './dist' # type: ignore
block_cipher = None
# CONF['workpath'] = './build'
# TODO: test on MAC OSX
data_files = [('../CHANGELOG.txt', '.')]
a = Analysis([f'{os.getcwd()}/src/music_caster.py'],
             pathex=[os.getcwd()],
             binaries=[],
             datas=data_files,
             hiddenimports=['pystray._win32', 'zeroconf._utils.ipaddress', 'zeroconf._handlers.answers'],
             hookspath=[],
             runtime_hooks=[],
             excludes=['crypto', 'cryptography', 'pycryptodome', 'pandas', 'gevent',
                       'numpy', 'simplejson', 'PySide2', 'PyQt5', 'greenlet'],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
a.datas.extend(Tree('src/templates', 'templates'))
a.datas.extend(Tree('src/static', 'static'))
VLC_EXCLUDES = ['*.dll', '*.so', '*.so*', '*.dylib*', '*.dylib']
if platform.system() == 'Windows':
    VLC_EXCLUDES.remove('*.dll')
elif platform.system() == 'Darwin':
    VLC_EXCLUDES.remove('*.dylib*')
    VLC_EXCLUDES.remove('*.dylib')
elif platform.system() == 'Linux':
    VLC_EXCLUDES.remove('*.so*')
    VLC_EXCLUDES.remove('*.so')
a.datas.extend(Tree('src/vlc_lib', 'vlc_lib', excludes=VLC_EXCLUDES))
a.datas.extend(Tree('src/languages', 'languages'))
a.datas.extend(Tree('build_files/tkdnd2.9.2', 'tkdnd2.9.2'))
a.datas.extend(Tree('src/theme', 'theme'))
# a.datas.extend(Tree('src-frontend/dist', 'frontend'))

pyz = PYZ(a.pure, a.zipped_data,
          cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='Music Caster',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=False,
          console=False, version='mc_version_info.txt', icon=os.path.abspath('resources/Music Caster Icon.ico'))
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=False,
               name='Music Caster OneDir')


================================================
FILE: build_files/portable.spec
================================================
# -*- mode: python ; coding: utf-8 -*-
import os
from PyInstaller.building.api import PYZ, EXE
from PyInstaller.building.build_main import Analysis, Tree # type: ignore
from PyInstaller.config import CONF
import platform

CONF['distpath'] = './dist' # type: ignore
# CONF['workpath'] = './build'
block_cipher = None
a = Analysis([f'{os.getcwd()}/src/music_caster.py'],
             pathex=[os.getcwd()],
             binaries=[],
             datas=[('../CHANGELOG.TXT', '.')],
             hiddenimports=['pystray._win32', 'zeroconf._utils.ipaddress', 'zeroconf._handlers.answers'],
             hookspath=[],
             runtime_hooks=[],
             excludes=['crypto', 'cryptography', 'pycryptodome', 'pandas', 'gevent',
                       'numpy', 'simplejson', 'PySide2', 'PyQt5', 'greenlet'],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

a.datas.extend(Tree('src/templates', 'templates'))
a.datas.extend(Tree('src/static', 'static'))
VLC_EXCLUDES = ['*.dll', '*.so', '*.so*', '*.dylib*', '*.dylib']
if platform.system() == 'Windows':
    VLC_EXCLUDES.remove('*.dll')
elif platform.system() == 'Darwin':
    VLC_EXCLUDES.remove('*.dylib*')
    VLC_EXCLUDES.remove('*.dylib')
elif platform.system() == 'Linux':
    VLC_EXCLUDES.remove('*.so*')
    VLC_EXCLUDES.remove('*.so')
a.datas.extend(Tree('src/vlc_lib', 'vlc_lib', excludes=VLC_EXCLUDES))
a.datas.extend(Tree('src/languages', 'languages'))
a.datas.extend(Tree('build_files/tkdnd2.9.2', 'tkdnd2.9.2'))
a.datas.extend(Tree('src/theme', 'theme'))
# a.datas.extend(Tree('src-frontend/dist', 'frontend'))

pyz = PYZ(a.pure, a.zipped_data,
          cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='Music Caster',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=False,
          runtime_tmpdir=None,
          # TODO: use ENV variable
          console=False, version='mc_version_info.txt', icon=os.path.abspath('resources/Music Caster Icon.ico'))


================================================
FILE: build_files/setup_script.iss
================================================
#define MyAppName "Music Caster"
#define MyAppVersion "5.25.1"
#define MyAppPublisher "Elijah Lopez"
#define MyAppURL "https://elijahlopez.ca/software#music-caster"
#define MyAppExeName "Music Caster.exe"

[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{FBE8A652-58D6-482D-B6A9-B3D7931CC9C5}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
Compression=lzma
SolidCompression=yes
WizardStyle=modern
MinVersion=0,6.1.7600
; Minimum version is Windows 10 or later
; Remove the following line to run in administrative install mode (install for all users.)
PrivilegesRequired=lowest
OutputDir={#SourcePath}\..\dist
OutputBaseFilename=Music Caster Setup
UninstallDisplayName=Music Caster
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallLogMode=overwrite
SetupIconFile="..\resources\Music Caster Icon.ico"

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[Files]
Source: "{#SourcePath}\..\dist\Music Caster OneDir\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
Source: "{#SourcePath}..\CHANGELOG.txt"; DestDir: "{app}"; DestName: "CHANGELOG.txt"; Flags: ignoreversion
; NOTE: Don't use "Flags: ignoreversion" on any shared system files

[InstallDelete]
Type: files; Name: {app}\*.pyd
Type: files; Name: {app}\*.dll
; delete previous version folders and those that may contain old files
Type: filesandordirs; Name: {app}\_internal
Type: filesandordirs; Name: {app}\Crypto
Type: filesandordirs; Name: {app}\Cryptodome
Type: filesandordirs; Name: {app}\google
Type: filesandordirs; Name: {app}\numpy
Type: filesandordirs; Name: {app}\PIL
Type: filesandordirs; Name: {app}\psutil
Type: filesandordirs; Name: {app}\gevent
Type: filesandordirs; Name: {app}\greenlet
Type: filesandordirs; Name: {app}\templates
Type: filesandordirs; Name: {app}\setuptools*
Type: filesandordirs; Name: {app}\images
Type: filesandordirs; Name: {app}\lib2to3
Type: filesandordirs; Name: {app}\lxml
Type: filesandordirs; Name: {app}\markupsafe
Type: filesandordirs; Name: {app}\pygame
Type: filesandordirs; Name: {app}\PyQt5
Type: filesandordirs; Name: {app}\wx
Type: filesandordirs; Name: {app}\vlc
Type: filesandordirs; Name: {app}\vlc_lib
Type: filesandordirs; Name: {app}\importlib_metadata*
Type: filesandordirs; Name: {app}\keyring*
Type: filesandordirs; Name: {app}\lz4*
Type: filesandordirs; Name: {app}\websockets*
Type: filesandordirs; Name: {app}\wheel*

[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent


================================================
FILE: build_files/tkdnd2.9.2/pkgIndex.tcl
================================================
package ifneeded tkdnd 2.9.2 \
  "source \{$dir/tkdnd.tcl\} ; \
   tkdnd::initialise \{$dir\} libtkdnd2.9.2[info sharedlibextension] tkdnd"

package ifneeded tkdnd::utils 2.9.2 \
  "source \{$dir/tkdnd_utils.tcl\} ; \
   package provide tkdnd::utils 2.9.2"


================================================
FILE: build_files/tkdnd2.9.2/tkdnd.tcl
================================================
#
# tkdnd.tcl --
#
#    This file implements some utility procedures that are used by the TkDND
#    package.
#
# This software is copyrighted by:
# George Petasis, National Centre for Scientific Research "Demokritos",
# Aghia Paraskevi, Athens, Greece.
# e-mail: petasis@iit.demokritos.gr
#
# The following terms apply to all files associated
# with the software unless explicitly disclaimed in individual files.
#
# The authors hereby grant permission to use, copy, modify, distribute,
# and license this software and its documentation for any purpose, provided
# that existing copyright notices are retained in all copies and that this
# notice is included verbatim in any distributions. No written agreement,
# license, or royalty fee is required for any of the authorized uses.
# Modifications to this software may be copyrighted by their authors
# and need not follow the licensing terms described here, provided that
# the new terms are clearly indicated on the first page of each file where
# they apply.
#
# IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
# ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
# DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
# IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
# NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
# MODIFICATIONS.
#

package require Tk

namespace eval ::tkdnd {
  variable _topw ".drag"
  variable _tabops
  variable _state
  variable _x0
  variable _y0
  variable _platform_namespace
  variable _drop_file_temp_dir
  variable _auto_update 1
  variable _dx 3 ;# The difference in pixels before a drag is initiated.
  variable _dy 3 ;# The difference in pixels before a drag is initiated.

  variable _windowingsystem

  bind TkDND_Drag1 <ButtonPress-1> {tkdnd::_begin_drag press  1 %W %s %X %Y %x %y}
  bind TkDND_Drag1 <B1-Motion>     {tkdnd::_begin_drag motion 1 %W %s %X %Y %x %y}
  bind TkDND_Drag2 <ButtonPress-2> {tkdnd::_begin_drag press  2 %W %s %X %Y %x %y}
  bind TkDND_Drag2 <B2-Motion>     {tkdnd::_begin_drag motion 2 %W %s %X %Y %x %y}
  bind TkDND_Drag3 <ButtonPress-3> {tkdnd::_begin_drag press  3 %W %s %X %Y %x %y}
  bind TkDND_Drag3 <B3-Motion>     {tkdnd::_begin_drag motion 3 %W %s %X %Y %x %y}

  # ----------------------------------------------------------------------------
  #  Command tkdnd::initialise: Initialise the TkDND package.
  # ----------------------------------------------------------------------------
  proc initialise { dir PKG_LIB_FILE PACKAGE_NAME} {
    variable _platform_namespace
    variable _drop_file_temp_dir
    variable _windowingsystem
    global env

    switch [tk windowingsystem] {
      x11 {
        set _windowingsystem x11
      }
      win32 -
      windows {
        set _windowingsystem windows
      }
      aqua  {
        set _windowingsystem aqua
      }
      default {
        error "unknown Tk windowing system"
      }
    }

    ## Get User's home directory: We try to locate the proper path from a set of
    ## environmental variables...
    foreach var {HOME HOMEPATH USERPROFILE ALLUSERSPROFILE APPDATA} {
      if {[info exists env($var)]} {
        if {[file isdirectory $env($var)]} {
          set UserHomeDir $env($var)
          break
        }
      }
    }

    ## Should use [tk windowingsystem] instead of tcl platform array:
    ## OS X returns "unix," but that's not useful because it has its own
    ## windowing system, aqua
    ## Under windows we have to also combine HOMEDRIVE & HOMEPATH...
    if {![info exists UserHomeDir] &&
        [string equal $_windowingsystem windows] &&
        [info exists env(HOMEDRIVE)] && [info exists env(HOMEPATH)]} {
      if {[file isdirectory $env(HOMEDRIVE)$env(HOMEPATH)]} {
        set UserHomeDir $env(HOMEDRIVE)$env(HOMEPATH)
      }
    }
    ## Have we located the needed path?
    if {![info exists UserHomeDir]} {
      set UserHomeDir [pwd]
    }
    set UserHomeDir [file normalize $UserHomeDir]

    ## Try to locate a temporary directory...
    foreach var {TKDND_TEMP_DIR TEMP TMP} {
      if {[info exists env($var)]} {
        if {[file isdirectory $env($var)] && [file writable $env($var)]} {
          set _drop_file_temp_dir $env($var)
          break
        }
      }
    }
    if {![info exists _drop_file_temp_dir]} {
      foreach _dir [list "$UserHomeDir/Local Settings/Temp" \
                         "$UserHomeDir/AppData/Local/Temp" \
                         /tmp \
                         C:/WINDOWS/Temp C:/Temp C:/tmp \
                         D:/WINDOWS/Temp D:/Temp D:/tmp] {
        if {[file isdirectory $_dir] && [file writable $_dir]} {
          set _drop_file_temp_dir $_dir
          break
        }
      }
    }
    if {![info exists _drop_file_temp_dir]} {
      set _drop_file_temp_dir $UserHomeDir
    }
    set _drop_file_temp_dir [file native $_drop_file_temp_dir]

    source $dir/tkdnd_generic.tcl
    switch $_windowingsystem {
      x11 {
        source $dir/tkdnd_unix.tcl
        set _platform_namespace xdnd
      }
      win32 -
      windows {
        source $dir/tkdnd_windows.tcl
        set _platform_namespace olednd
      }
      aqua  {
        source $dir/tkdnd_macosx.tcl
        set _platform_namespace macdnd
      }
      default {
        error "unknown Tk windowing system"
      }
    }
    load $dir/$PKG_LIB_FILE $PACKAGE_NAME
    source $dir/tkdnd_compat.tcl
    ${_platform_namespace}::initialise
  };# initialise

  proc GetDropFileTempDirectory { } {
    variable _drop_file_temp_dir
    return $_drop_file_temp_dir
  }
  proc SetDropFileTempDirectory { dir } {
    variable _drop_file_temp_dir
    set _drop_file_temp_dir $dir
  }

};# namespace ::tkdnd

# ----------------------------------------------------------------------------
#  Command tkdnd::drag_source
# ----------------------------------------------------------------------------
proc ::tkdnd::drag_source { mode path { types {} } { event 1 }
                                      { tagprefix TkDND_Drag } } {
  set tags [bindtags $path]
  set idx  [lsearch $tags ${tagprefix}$event]
  switch -- $mode {
    register {
      if { $idx != -1 } {
        ## No need to do anything!
        # bindtags $path [lreplace $tags $idx $idx ${tagprefix}$event]
      } else {
        bindtags $path [linsert $tags 1 ${tagprefix}$event]
      }
      _drag_source_update_types $path $types
    }
    unregister {
      if { $idx != -1 } {
        bindtags $path [lreplace $tags $idx $idx]
      }
    }
  }
};# tkdnd::drag_source

proc ::tkdnd::_drag_source_update_types { path types } {
  set types [platform_specific_types $types]
  set old_types [bind $path <<DragSourceTypes>>]
  foreach type $types {
    if {[lsearch $old_types $type] < 0} {lappend old_types $type}
  }
  bind $path <<DragSourceTypes>> $old_types
};# ::tkdnd::_drag_source_update_types

# ----------------------------------------------------------------------------
#  Command tkdnd::drop_target
# ----------------------------------------------------------------------------
proc ::tkdnd::drop_target { mode path { types {} } } {
  variable _windowingsystem
  set types [platform_specific_types $types]
  switch -- $mode {
    register {
      switch $_windowingsystem {
        x11 {
          _register_types $path [winfo toplevel $path] $types
        }
        win32 -
        windows {
          _RegisterDragDrop $path
          bind <Destroy> $path {+ tkdnd::_RevokeDragDrop %W}
        }
        aqua {
          macdnd::registerdragwidget [winfo toplevel $path] $types
        }
        default {
          error "unknown Tk windowing system"
        }
      }
      set old_types [bind $path <<DropTargetTypes>>]
      set new_types {}
      foreach type $types {
        if {[lsearch -exact $old_types $type] < 0} {lappend new_types $type}
      }
      if {[llength $new_types]} {
        bind $path <<DropTargetTypes>> [concat $old_types $new_types]
      }
    }
    unregister {
      switch $_windowingsystem {
        x11 {
        }
        win32 -
        windows {
          _RevokeDragDrop $path
        }
        aqua {
          error todo
        }
        default {
          error "unknown Tk windowing system"
        }
      }
      bind $path <<DropTargetTypes>> {}
    }
  }
};# tkdnd::drop_target

# ----------------------------------------------------------------------------
#  Command tkdnd::_begin_drag
# ----------------------------------------------------------------------------
proc ::tkdnd::_begin_drag { event button source state X Y x y } {
  variable _x0
  variable _y0
  variable _state

  switch -- $event {
    press {
      set _x0    $X
      set _y0    $Y
      set _state "press"
    }
    motion {
      if { ![info exists _state] } {
        # This is just extra protection. There seem to be
        # rare cases where the motion comes before the press.
        return
      }
      if { [string equal $_state "press"] } {
        variable _dx
        variable _dy
        if { abs($_x0-$X) > ${_dx} || abs($_y0-$Y) > ${_dy} } {
          set _state "done"
          _init_drag $button $source $state $X $Y $x $y
        }
      }
    }
  }
};# tkdnd::_begin_drag

# ----------------------------------------------------------------------------
#  Command tkdnd::_init_drag
# ----------------------------------------------------------------------------
proc ::tkdnd::_init_drag { button source state rootX rootY X Y } {
  # Call the <<DragInitCmd>> binding.
  set cmd [bind $source <<DragInitCmd>>]
  # puts "CMD: $cmd"
  if {[string length $cmd]} {
    set cmd [string map [list %W $source %X $rootX %Y $rootY %x $X %y $Y \
                              %S $state  %e <<DragInitCmd>> %A \{\} %% % \
                              %t [bind $source <<DragSourceTypes>>]] $cmd]
    set code [catch {uplevel \#0 $cmd} info options]
    # puts "CODE: $code ---- $info"
    switch -exact -- $code {
      0 {}
      3 - 4 {
        # FRINK: nocheck
        return
      }
      default {
        return -options $options $info
      }
    }

    set len [llength $info]
    if {$len == 3} {
      foreach { actions types _data } $info { break }
      set types [platform_specific_types $types]
      set data [list]
      foreach type $types {
        lappend data $_data
      }
      unset _data
    } elseif {$len == 2} {
      foreach { actions _data } $info { break }
      set data [list]; set types [list]
      foreach {t d} $_data {
        foreach t [platform_specific_types $t] {
          lappend types $t; lappend data $d
        }
      }
      unset _data t d
    } else {
      if {$len == 1 && [string equal [lindex $actions 0] "refuse_drop"]} {
        return
      }
      error "not enough items in the result of the <<DragInitCmd>>\
             event binding. Either 2 or 3 items are expected. The command
             executed was: \"$cmd\"\nResult was: \"$info\""
    }
    set action refuse_drop
    variable _windowingsystem
    # puts "Source:   \"$source\""
    # puts "Types:    \"[join $types {", "}]\""
    # puts "Actions:  \"[join $actions {", "}]\""
    # puts "Button:   \"$button\""
    # puts "Data:     \"[string range $data 0 100]\""
    switch $_windowingsystem {
      x11 {
        set action [xdnd::_dodragdrop $source $actions $types $data $button]
      }
      win32 -
      windows {
        set action [_DoDragDrop $source $actions $types $data $button]
      }
      aqua {
        set action [macdnd::dodragdrop $source $actions $types $data $button]
      }
      default {
        error "unknown Tk windowing system"
      }
    }
    ## Call _end_drag to notify the widget of the result of the drag
    ## operation...
    _end_drag $button $source {} $action {} $data {} $state $rootX $rootY $X $Y
  }
};# tkdnd::_init_drag

# ----------------------------------------------------------------------------
#  Command tkdnd::_end_drag
# ----------------------------------------------------------------------------
proc ::tkdnd::_end_drag { button source target action type data result
                          state rootX rootY X Y } {
  set rootX 0
  set rootY 0
  # Call the <<DragEndCmd>> binding.
  set cmd [bind $source <<DragEndCmd>>]
  if {[string length $cmd]} {
    set cmd [string map [list %W $source %X $rootX %Y $rootY %x $X %y $Y %% % \
                              %S $state %e <<DragEndCmd>> %A \{$action\}] $cmd]
    set info [uplevel \#0 $cmd]
    # if { $info != "" } {
    #   variable _windowingsystem
    #   foreach { actions types data } $info { break }
    #   set types [platform_specific_types $types]
    #   switch $_windowingsystem {
    #     x11 {
    #       error "dragging from Tk widgets not yet supported"
    #     }
    #     win32 -
    #     windows {
    #       set action [_DoDragDrop $source $actions $types $data $button]
    #     }
    #     aqua {
    #       macdnd::dodragdrop $source $actions $types $data
    #     }
    #     default {
    #       error "unknown Tk windowing system"
    #     }
    #   }
    #   ## Call _end_drag to notify the widget of the result of the drag
    #   ## operation...
    #   _end_drag $button $source {} $action {} $data {} $state $rootX $rootY
    # }
  }
};# tkdnd::_end_drag

# ----------------------------------------------------------------------------
#  Command tkdnd::platform_specific_types
# ----------------------------------------------------------------------------
proc ::tkdnd::platform_specific_types { types } {
  variable _platform_namespace
  ${_platform_namespace}::platform_specific_types $types
}; # tkdnd::platform_specific_types

# ----------------------------------------------------------------------------
#  Command tkdnd::platform_independent_types
# ----------------------------------------------------------------------------
proc ::tkdnd::platform_independent_types { types } {
  variable _platform_namespace
  ${_platform_namespace}::platform_independent_types $types
}; # tkdnd::platform_independent_types

# ----------------------------------------------------------------------------
#  Command tkdnd::platform_specific_type
# ----------------------------------------------------------------------------
proc ::tkdnd::platform_specific_type { type } {
  variable _platform_namespace
  ${_platform_namespace}::platform_specific_type $type
}; # tkdnd::platform_specific_type

# ----------------------------------------------------------------------------
#  Command tkdnd::platform_independent_type
# ----------------------------------------------------------------------------
proc ::tkdnd::platform_independent_type { type } {
  variable _platform_namespace
  ${_platform_namespace}::platform_independent_type $type
}; # tkdnd::platform_independent_type

# ----------------------------------------------------------------------------
#  Command tkdnd::bytes_to_string
# ----------------------------------------------------------------------------
proc ::tkdnd::bytes_to_string { bytes } {
  set string {}
  foreach byte $bytes {
    append string [binary format c $byte]
  }
  return $string
};# tkdnd::bytes_to_string

# ----------------------------------------------------------------------------
#  Command tkdnd::urn_unquote
# ----------------------------------------------------------------------------
proc ::tkdnd::urn_unquote {url} {
  set result ""
  set start 0
  while {[regexp -start $start -indices {%[0-9a-fA-F]{2}} $url match]} {
    foreach {first last} $match break
    append result [string range $url $start [expr {$first - 1}]]
    append result [format %c 0x[string range $url [incr first] $last]]
    set start [incr last]
  }
  append result [string range $url $start end]
  return [encoding convertfrom utf-8 $result]
};# tkdnd::urn_unquote


================================================
FILE: build_files/tkdnd2.9.2/tkdnd_compat.tcl
================================================
#
# tkdnd_compat.tcl --
# 
#    This file implements some utility procedures, to support older versions
#    of the TkDND package.
#
# This software is copyrighted by:
# George Petasis, National Centre for Scientific Research "Demokritos",
# Aghia Paraskevi, Athens, Greece.
# e-mail: petasis@iit.demokritos.gr
#
# The following terms apply to all files associated
# with the software unless explicitly disclaimed in individual files.
#
# The authors hereby grant permission to use, copy, modify, distribute,
# and license this software and its documentation for any purpose, provided
# that existing copyright notices are retained in all copies and that this
# notice is included verbatim in any distributions. No written agreement,
# license, or royalty fee is required for any of the authorized uses.
# Modifications to this software may be copyrighted by their authors
# and need not follow the licensing terms described here, provided that
# the new terms are clearly indicated on the first page of each file where
# they apply.
# 
# IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
# ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
# DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# 
# THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
# IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
# NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
# MODIFICATIONS.
#

namespace eval compat {

};# namespace compat

# ----------------------------------------------------------------------------
#  Command ::dnd
# ----------------------------------------------------------------------------
proc ::dnd {method window args} {
  switch $method {
    bindtarget {
      switch [llength $args] {
        0 {return [tkdnd::compat::bindtarget0 $window]}
        1 {return [tkdnd::compat::bindtarget1 $window [lindex $args 0]]}
        2 {return [tkdnd::compat::bindtarget2 $window [lindex $args 0] \
                                                      [lindex $args 1]]}
        3 {return [tkdnd::compat::bindtarget3 $window [lindex $args 0] \
                                     [lindex $args 1] [lindex $args 2]]}
        4 {return [tkdnd::compat::bindtarget4 $window [lindex $args 0] \
                    [lindex $args 1] [lindex $args 2] [lindex $args 3]]}
      }
    }
    cleartarget {
      return [tkdnd::compat::cleartarget $window]
    }
    bindsource {
      switch [llength $args] {
        0 {return [tkdnd::compat::bindsource0 $window]}
        1 {return [tkdnd::compat::bindsource1 $window [lindex $args 0]]}
        2 {return [tkdnd::compat::bindsource2 $window [lindex $args 0] \
                                                      [lindex $args 1]]}
        3 {return [tkdnd::compat::bindsource3 $window [lindex $args 0] \
                                     [lindex $args 1] [lindex $args 2]]}
      }
    }
    clearsource {
      return [tkdnd::compat::clearsource $window]
    }
    drag {
      return [tkdnd::_init_drag 1 $window "press" 0 0 0 0]
    }
  }
  error "invalid number of arguments!"
};# ::dnd

# ----------------------------------------------------------------------------
#  Command compat::bindtarget
# ----------------------------------------------------------------------------
proc compat::bindtarget0 {window} {
  return [bind $window <<DropTargetTypes>>]
};# compat::bindtarget0

proc compat::bindtarget1 {window type} {
  return [bindtarget2 $window $type <Drop>]
};# compat::bindtarget1

proc compat::bindtarget2 {window type event} {
  switch $event {
    <DragEnter> {return [bind $window <<DropEnter>>]}
    <Drag>      {return [bind $window <<DropPosition>>]}
    <DragLeave> {return [bind $window <<DropLeave>>]}
    <Drop>      {return [bind $window <<Drop>>]}
  }
};# compat::bindtarget2

proc compat::bindtarget3 {window type event script} {
  set type [normalise_type $type]
  ::tkdnd::drop_target register $window [list $type]
  switch $event {
    <DragEnter> {return [bind $window <<DropEnter>> $script]}
    <Drag>      {return [bind $window <<DropPosition>> $script]}
    <DragLeave> {return [bind $window <<DropLeave>> $script]}
    <Drop>      {return [bind $window <<Drop>> $script]}
  }
};# compat::bindtarget3

proc compat::bindtarget4 {window type event script priority} {
  return [bindtarget3 $window $type $event $script]
};# compat::bindtarget4

proc compat::normalise_type { type } {
  switch $type {
    text/plain -
    {text/plain;charset=UTF-8} -
    Text                       {return DND_Text}
    text/uri-list -
    Files                      {return DND_Files}
    default                    {return $type}
  }
};# compat::normalise_type

# ----------------------------------------------------------------------------
#  Command compat::bindsource
# ----------------------------------------------------------------------------
proc compat::bindsource0 {window} {
  return [bind $window <<DropTargetTypes>>]
};# compat::bindsource0

proc compat::bindsource1 {window type} {
  return [bindsource2 $window $type <Drop>]
};# compat::bindsource1

proc compat::bindsource2 {window type script} {
  set type [normalise_type $type]
  ::tkdnd::drag_source register $window $type
  bind $window <<DragInitCmd>> "list {copy} {%t} \[$script\]"
};# compat::bindsource2

proc compat::bindsource3 {window type script priority} {
  return [bindsource2 $window $type $script]
};# compat::bindsource3

# ----------------------------------------------------------------------------
#  Command compat::cleartarget
# ----------------------------------------------------------------------------
proc compat::cleartarget {window} {
};# compat::cleartarget

# ----------------------------------------------------------------------------
#  Command compat::clearsource
# ----------------------------------------------------------------------------
proc compat::clearsource {window} {
};# compat::clearsource


================================================
FILE: build_files/tkdnd2.9.2/tkdnd_generic.tcl
================================================
#
# tkdnd_generic.tcl --
#
#    This file implements some utility procedures that are used by the TkDND
#    package.
#
# This software is copyrighted by:
# George Petasis, National Centre for Scientific Research "Demokritos",
# Aghia Paraskevi, Athens, Greece.
# e-mail: petasis@iit.demokritos.gr
#
# The following terms apply to all files associated
# with the software unless explicitly disclaimed in individual files.
#
# The authors hereby grant permission to use, copy, modify, distribute,
# and license this software and its documentation for any purpose, provided
# that existing copyright notices are retained in all copies and that this
# notice is included verbatim in any distributions. No written agreement,
# license, or royalty fee is required for any of the authorized uses.
# Modifications to this software may be copyrighted by their authors
# and need not follow the licensing terms described here, provided that
# the new terms are clearly indicated on the first page of each file where
# they apply.
#
# IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
# ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
# DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
# IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
# NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
# MODIFICATIONS.
#

namespace eval generic {
  variable _types {}
  variable _typelist {}
  variable _codelist {}
  variable _actionlist {}
  variable _pressedkeys {}
  variable _action {}
  variable _common_drag_source_types {}
  variable _common_drop_target_types {}
  variable _drag_source {}
  variable _drop_target {}

  variable _last_mouse_root_x 0
  variable _last_mouse_root_y 0

  variable _tkdnd2platform
  variable _platform2tkdnd

  proc debug {msg} {
    puts $msg
  };# debug

  proc initialise { } {
  };# initialise

  proc initialise_platform_to_tkdnd_types { types } {
    variable _platform2tkdnd
    variable _tkdnd2platform
    set _platform2tkdnd [dict create {*}$types]
    set _tkdnd2platform [dict create]
    foreach type [dict keys $_platform2tkdnd] {
      dict lappend _tkdnd2platform [dict get $_platform2tkdnd $type] $type
    }
  };# initialise_platform_to_tkdnd_types

  proc initialise_tkdnd_to_platform_types { types } {
    variable _tkdnd2platform
    set _tkdnd2platform [dict create {*}$types]
  };# initialise_tkdnd_to_platform_types

};# namespace generic

# ----------------------------------------------------------------------------
#  Command generic::HandleEnter
# ----------------------------------------------------------------------------
proc generic::HandleEnter { drop_target drag_source typelist codelist
                            actionlist pressedkeys } {
  variable _typelist;                 set _typelist    $typelist
  variable _pressedkeys;              set _pressedkeys $pressedkeys
  variable _action;                   set _action      refuse_drop
  variable _common_drag_source_types; set _common_drag_source_types {}
  variable _common_drop_target_types; set _common_drop_target_types {}
  variable _actionlist
  variable _drag_source;              set _drag_source $drag_source
  variable _drop_target;              set _drop_target {}
  variable _actionlist;               set _actionlist  $actionlist
  variable _codelist                  set _codelist    $codelist

  variable _last_mouse_root_x;        set _last_mouse_root_x 0
  variable _last_mouse_root_y;        set _last_mouse_root_y 0
  # debug "\n==============================================================="
  # debug "generic::HandleEnter: drop_target=$drop_target,\
  #        drag_source=$drag_source,\
  #        typelist=$typelist"
  # debug "generic::HandleEnter: ACTION: default"
  return default
};# generic::HandleEnter

# ----------------------------------------------------------------------------
#  Command generic::HandlePosition
# ----------------------------------------------------------------------------
proc generic::HandlePosition { drop_target drag_source pressedkeys
                               rootX rootY { time 0 } } {
  variable _types
  variable _typelist
  variable _codelist
  variable _actionlist
  variable _pressedkeys
  variable _action
  variable _common_drag_source_types
  variable _common_drop_target_types
  variable _drag_source
  variable _drop_target

  variable _last_mouse_root_x;        set _last_mouse_root_x $rootX
  variable _last_mouse_root_y;        set _last_mouse_root_y $rootY

  # debug "generic::HandlePosition: drop_target=$drop_target,\
  #            _drop_target=$_drop_target, rootX=$rootX, rootY=$rootY"

  if {![info exists _drag_source] && ![string length $_drag_source]} {
    # debug "generic::HandlePosition: no or empty _drag_source:\
    #               return refuse_drop"
    return refuse_drop
  }

  if {$drag_source ne "" && $drag_source ne $_drag_source} {
    debug "generic position event from unexpected source: $_drag_source\
           != $drag_source"
    return refuse_drop
  }

  set _pressedkeys $pressedkeys

  ## Does the new drop target support any of our new types?
  # foreach {common_drag_source_types common_drop_target_types} \
  #         [GetWindowCommonTypes $drop_target $_typelist] {break}
  foreach {drop_target common_drag_source_types common_drop_target_types} \
          [FindWindowWithCommonTypes $drop_target $_typelist] {break}
  set data [GetDroppedData $time]

  # debug "\t($_drop_target) -> ($drop_target)"
  if {$drop_target != $_drop_target} {
    if {[string length $_drop_target]} {
      ## Call the <<DropLeave>> event.
      # debug "\t<<DropLeave>> on $_drop_target"
      set cmd [bind $_drop_target <<DropLeave>>]
      if {[string length $cmd]} {
        set cmd [string map [list %W $_drop_target %X $rootX %Y $rootY \
          %CST \{$_common_drag_source_types\} \
          %CTT \{$_common_drop_target_types\} \
          %CPT \{[lindex [platform_independent_type [lindex $_common_drag_source_types 0]] 0]\} \
          %ST  \{$_typelist\}    %TT \{$_types\} \
          %A   \{$_action\}      %a \{$_actionlist\} \
          %b   \{$_pressedkeys\} %m \{$_pressedkeys\} \
          %D   \{\}              %e <<DropLeave>> \
          %L   \{$_typelist\}    %% % \
          %t   \{$_typelist\}    %T  \{[lindex $_common_drag_source_types 0]\} \
          %c   \{$_codelist\}    %C  \{[lindex $_codelist 0]\} \
          ] $cmd]
        uplevel \#0 $cmd
      }
    }
    set _drop_target $drop_target
    set _action      refuse_drop

    if {[llength $common_drag_source_types]} {
      set _action [lindex $_actionlist 0]
      set _common_drag_source_types $common_drag_source_types
      set _common_drop_target_types $common_drop_target_types
      ## Drop target supports at least one type. Send a <<DropEnter>>.
      # puts "<<DropEnter>> -> $drop_target"
      set cmd [bind $drop_target <<DropEnter>>]
      if {[string length $cmd]} {
        focus $drop_target
        set cmd [string map [list %W $drop_target %X $rootX %Y $rootY \
          %CST \{$_common_drag_source_types\} \
          %CTT \{$_common_drop_target_types\} \
          %CPT \{[lindex [platform_independent_type [lindex $_common_drag_source_types 0]] 0]\} \
          %ST  \{$_typelist\}    %TT \{$_types\} \
          %A   $_action          %a  \{$_actionlist\} \
          %b   \{$_pressedkeys\} %m  \{$_pressedkeys\} \
          %D   [list $data]      %e  <<DropEnter>> \
          %L   \{$_typelist\}    %%  % \
          %t   \{$_typelist\}    %T  \{[lindex $_common_drag_source_types 0]\} \
          %c   \{$_codelist\}    %C  \{[lindex $_codelist 0]\} \
          ] $cmd]
        set _action [uplevel \#0 $cmd]
        switch -exact -- $_action {
          copy - move - link - ask - private - refuse_drop - default {}
          default {set _action copy}
        }
      }
    }
  }

  set _drop_target {}
  if {[llength $common_drag_source_types]} {
    set _common_drag_source_types $common_drag_source_types
    set _common_drop_target_types $common_drop_target_types
    set _drop_target $drop_target
    ## Drop target supports at least one type. Send a <<DropPosition>>.
    set cmd [bind $drop_target <<DropPosition>>]
    if {[string length $cmd]} {
      set cmd [string map [list %W $drop_target %X $rootX %Y $rootY \
        %CST \{$_common_drag_source_types\} \
        %CTT \{$_common_drop_target_types\} \
        %CPT \{[lindex [platform_independent_type [lindex $_common_drag_source_types 0]] 0]\} \
        %ST  \{$_typelist\}    %TT \{$_types\} \
        %A   $_action          %a  \{$_actionlist\} \
        %b   \{$_pressedkeys\} %m  \{$_pressedkeys\} \
        %D   [list $data]      %e  <<DropPosition>> \
        %L   \{$_typelist\}    %%  % \
        %t   \{$_typelist\}    %T  \{[lindex $_common_drag_source_types 0]\} \
        %c   \{$_codelist\}    %C  \{[lindex $_codelist 0]\} \
        ] $cmd]
      set _action [uplevel \#0 $cmd]
    }
  }
  # Return values: copy, move, link, ask, private, refuse_drop, default
  # debug "generic::HandlePosition: ACTION: $_action"
  switch -exact -- $_action {
    copy - move - link - ask - private - refuse_drop - default {}
    default {set _action copy}
  }
  return $_action
};# generic::HandlePosition

# ----------------------------------------------------------------------------
#  Command generic::HandleLeave
# ----------------------------------------------------------------------------
proc generic::HandleLeave { } {
  variable _types
  variable _typelist
  variable _codelist
  variable _actionlist
  variable _pressedkeys
  variable _action
  variable _common_drag_source_types
  variable _common_drop_target_types
  variable _drag_source
  variable _drop_target
  variable _last_mouse_root_x
  variable _last_mouse_root_y
  if {![info exists _drop_target]} {set _drop_target {}}
  # debug "generic::HandleLeave: _drop_target=$_drop_target"
  if {[info exists _drop_target] && [string length $_drop_target]} {
    set cmd [bind $_drop_target <<DropLeave>>]
    if {[string length $cmd]} {
      set cmd [string map [list %W $_drop_target \
        %X $_last_mouse_root_x %Y $_last_mouse_root_y \
        %CST \{$_common_drag_source_types\} \
        %CTT \{$_common_drop_target_types\} \
        %CPT \{[lindex [platform_independent_type [lindex $_common_drag_source_types 0]] 0]\} \
        %ST  \{$_typelist\}    %TT \{$_types\} \
        %A   \{$_action\}      %a  \{$_actionlist\} \
        %b   \{$_pressedkeys\} %m  \{$_pressedkeys\} \
        %D   \{\}              %e  <<DropLeave>> \
        %L   \{$_typelist\}    %%  % \
        %t   \{$_typelist\}    %T  \{[lindex $_common_drag_source_types 0]\} \
        %c   \{$_codelist\}    %C  \{[lindex $_codelist 0]\} \
        ] $cmd]
      set _action [uplevel \#0 $cmd]
    }
  }
  foreach var {_types _typelist _actionlist _pressedkeys _action
               _common_drag_source_types _common_drop_target_types
               _drag_source _drop_target} {
    set $var {}
  }
};# generic::HandleLeave

# ----------------------------------------------------------------------------
#  Command generic::HandleDrop
# ----------------------------------------------------------------------------
proc generic::HandleDrop {drop_target drag_source pressedkeys rootX rootY time } {
  variable _types
  variable _typelist
  variable _codelist
  variable _actionlist
  variable _pressedkeys
  variable _action
  variable _common_drag_source_types
  variable _common_drop_target_types
  variable _drag_source
  variable _drop_target
  variable _last_mouse_root_x
  variable _last_mouse_root_y
  variable _last_mouse_root_x;        set _last_mouse_root_x $rootX
  variable _last_mouse_root_y;        set _last_mouse_root_y $rootY

  set _pressedkeys $pressedkeys

  # puts "generic::HandleDrop: $time"

  if {![info exists _drag_source] && ![string length $_drag_source]} {
    return refuse_drop
  }
  if {![info exists _drop_target] && ![string length $_drop_target]} {
    return refuse_drop
  }
  if {![llength $_common_drag_source_types]} {return refuse_drop}
  ## Get the dropped data.
  set data [GetDroppedData $time]
  ## Try to select the most specific <<Drop>> event.
  foreach type [concat $_common_drag_source_types $_common_drop_target_types] {
    set type [platform_independent_type $type]
    set cmd [bind $_drop_target <<Drop:$type>>]
    if {[string length $cmd]} {
      set cmd [string map [list %W $_drop_target %X $rootX %Y $rootY \
        %CST \{$_common_drag_source_types\} \
        %CTT \{$_common_drop_target_types\} \
        %CPT \{[lindex [platform_independent_type [lindex $_common_drag_source_types 0]] 0]\} \
        %ST  \{$_typelist\}    %TT \{$_types\} \
        %A   $_action          %a \{$_actionlist\} \
        %b   \{$_pressedkeys\} %m \{$_pressedkeys\} \
        %D   [list $data]      %e <<Drop:$type>> \
        %L   \{$_typelist\}    %% % \
        %t   \{$_typelist\}    %T  \{[lindex $_common_drag_source_types 0]\} \
        %c   \{$_codelist\}    %C  \{[lindex $_codelist 0]\} \
        ] $cmd]
      set _action [uplevel \#0 $cmd]
      # Return values: copy, move, link, ask, private, refuse_drop
      switch -exact -- $_action {
        copy - move - link - ask - private - refuse_drop - default {}
        default {set _action copy}
      }
      return $_action
    }
  }
  set cmd [bind $_drop_target <<Drop>>]
  if {[string length $cmd]} {
    set cmd [string map [list %W $_drop_target %X $rootX %Y $rootY \
      %CST \{$_common_drag_source_types\} \
      %CTT \{$_common_drop_target_types\} \
      %CPT \{[lindex [platform_independent_type [lindex $_common_drag_source_types 0]] 0]\} \
      %ST  \{$_typelist\}    %TT \{$_types\} \
      %A   $_action          %a \{$_actionlist\} \
      %b   \{$_pressedkeys\} %m \{$_pressedkeys\} \
      %D   [list $data]      %e <<Drop>> \
      %L   \{$_typelist\}    %% % \
      %t   \{$_typelist\}    %T  \{[lindex $_common_drag_source_types 0]\} \
      %c   \{$_codelist\}    %C  \{[lindex $_codelist 0]\} \
      ] $cmd]
    set _action [uplevel \#0 $cmd]
  }
  # Return values: copy, move, link, ask, private, refuse_drop
  switch -exact -- $_action {
    copy - move - link - ask - private - refuse_drop - default {}
    default {set _action copy}
  }
  return $_action
};# generic::HandleDrop

# ----------------------------------------------------------------------------
#  Command generic::GetWindowCommonTypes
# ----------------------------------------------------------------------------
proc generic::GetWindowCommonTypes { win typelist } {
  set types [bind $win <<DropTargetTypes>>]
  # debug ">> Accepted types: $win $_types"
  set common_drag_source_types {}
  set common_drop_target_types {}
  if {[llength $types]} {
    ## Examine the drop target types, to find at least one match with the drag
    ## source types...
    set supported_types [supported_types $typelist]
    foreach type $types {
      foreach matched [lsearch -glob -all -inline $supported_types $type] {
        ## Drop target supports this type.
        lappend common_drag_source_types $matched
        lappend common_drop_target_types $type
      }
    }
  }
  list $common_drag_source_types $common_drop_target_types
};# generic::GetWindowCommonTypes

# ----------------------------------------------------------------------------
#  Command generic::FindWindowWithCommonTypes
# ----------------------------------------------------------------------------
proc generic::FindWindowWithCommonTypes { win typelist } {
  set toplevel [winfo toplevel $win]
  while {![string equal $win $toplevel]} {
    foreach {common_drag_source_types common_drop_target_types} \
            [GetWindowCommonTypes $win $typelist] {break}
    if {[llength $common_drag_source_types]} {
      return [list $win $common_drag_source_types $common_drop_target_types]
    }
    set win [winfo parent $win]
  }
  ## We have reached the toplevel, which may be also a target (SF Bug #30)
  foreach {common_drag_source_types common_drop_target_types} \
          [GetWindowCommonTypes $win $typelist] {break}
  if {[llength $common_drag_source_types]} {
    return [list $win $common_drag_source_types $common_drop_target_types]
  }
  return { {} {} {} }
};# generic::FindWindowWithCommonTypes

# ----------------------------------------------------------------------------
#  Command generic::GetDroppedData
# ----------------------------------------------------------------------------
proc generic::GetDroppedData { time } {
  variable _dropped_data
  return  $_dropped_data
};# generic::GetDroppedData

# ----------------------------------------------------------------------------
#  Command generic::SetDroppedData
# ----------------------------------------------------------------------------
proc generic::SetDroppedData { data } {
  variable _dropped_data
  set _dropped_data $data
};# generic::SetDroppedData

# ----------------------------------------------------------------------------
#  Command generic::GetDragSource
# ----------------------------------------------------------------------------
proc generic::GetDragSource { } {
  variable _drag_source
  return  $_drag_source
};# generic::GetDragSource

# ----------------------------------------------------------------------------
#  Command generic::GetDropTarget
# ----------------------------------------------------------------------------
proc generic::GetDropTarget { } {
  variable _drop_target
  return $_drop_target
};# generic::GetDropTarget

# ----------------------------------------------------------------------------
#  Command generic::GetDragSourceCommonTypes
# ----------------------------------------------------------------------------
proc generic::GetDragSourceCommonTypes { } {
  variable _common_drag_source_types
  return  $_common_drag_source_types
};# generic::GetDragSourceCommonTypes

# ----------------------------------------------------------------------------
#  Command generic::GetDropTargetCommonTypes
# ----------------------------------------------------------------------------
proc generic::GetDropTargetCommonTypes { } {
  variable _common_drag_source_types
  return  $_common_drag_source_types
};# generic::GetDropTargetCommonTypes

# ----------------------------------------------------------------------------
#  Command generic::platform_specific_types
# ----------------------------------------------------------------------------
proc generic::platform_specific_types { types } {
  set new_types {}
  foreach type $types {
    set new_types [concat $new_types [platform_specific_type $type]]
  }
  return $new_types
}; # generic::platform_specific_types

# ----------------------------------------------------------------------------
#  Command generic::platform_specific_type
# ----------------------------------------------------------------------------
proc generic::platform_specific_type { type } {
  variable _tkdnd2platform
  if {[dict exists $_tkdnd2platform $type]} {
    return [dict get $_tkdnd2platform $type]
  }
  list $type
}; # generic::platform_specific_type

# ----------------------------------------------------------------------------
#  Command tkdnd::platform_independent_types
# ----------------------------------------------------------------------------
proc ::tkdnd::platform_independent_types { types } {
  set new_types {}
  foreach type $types {
    set new_types [concat $new_types [platform_independent_type $type]]
  }
  return $new_types
}; # tkdnd::platform_independent_types

# ----------------------------------------------------------------------------
#  Command generic::platform_independent_type
# ----------------------------------------------------------------------------
proc generic::platform_independent_type { type } {
  variable _platform2tkdnd
  if {[dict exists $_platform2tkdnd $type]} {
    return [dict get $_platform2tkdnd $type]
  }
  return $type
}; # generic::platform_independent_type

# ----------------------------------------------------------------------------
#  Command generic::supported_types
# ----------------------------------------------------------------------------
proc generic::supported_types { types } {
  set new_types {}
  foreach type $types {
    if {[supported_type $type]} {lappend new_types $type}
  }
  return $new_types
}; # generic::supported_types

# ----------------------------------------------------------------------------
#  Command generic::supported_type
# ----------------------------------------------------------------------------
proc generic::supported_type { type } {
  variable _platform2tkdnd
  if {[dict exists $_platform2tkdnd $type]} {
    return 1
  }
  return 0
}; # generic::supported_type


================================================
FILE: build_files/tkdnd2.9.2/tkdnd_macosx.tcl
================================================
#
# tkdnd_macosx.tcl --
#
#    This file implements some utility procedures that are used by the TkDND
#    package.

#   This software is copyrighted by:
#   Georgios Petasis, Athens, Greece.
#   e-mail: petasisg@yahoo.gr, petasis@iit.demokritos.gr
#
#   Mac portions (c) 2009 Kevin Walzer/WordTech Communications LLC,
#   kw@codebykevin.com
#
#
# The following terms apply to all files associated
# with the software unless explicitly disclaimed in individual files.
#
# The authors hereby grant permission to use, copy, modify, distribute,
# and license this software and its documentation for any purpose, provided
# that existing copyright notices are retained in all copies and that this
# notice is included verbatim in any distributions. No written agreement,
# license, or royalty fee is required for any of the authorized uses.
# Modifications to this software may be copyrighted by their authors
# and need not follow the licensing terms described here, provided that
# the new terms are clearly indicated on the first page of each file where
# they apply.
#
# IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
# ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
# DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
# IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
# NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
# MODIFICATIONS.
#

#basic API for Mac Drag and Drop

#two data types supported: strings and file paths

#two commands at C level: ::tkdnd::macdnd::registerdragwidget, ::tkdnd::macdnd::unregisterdragwidget

#data retrieval mechanism: text or file paths are copied from drag clipboard to system clipboard and retrieved via [clipboard get]; array of file paths is converted to single tab-separated string, can be split into Tcl list

if {[tk windowingsystem] eq "aqua" && "AppKit" ni [winfo server .]} {
  error {TkAqua Cocoa required}
}

namespace eval macdnd {

  proc initialise { } {
     ## Mapping from platform types to TkDND types...
    ::tkdnd::generic::initialise_platform_to_tkdnd_types [list \
       NSPasteboardTypeString  DND_Text  \
       NSFilenamesPboardType   DND_Files \
       NSPasteboardTypeHTML    DND_HTML  \
    ]
  };# initialise

};# namespace macdnd

# ----------------------------------------------------------------------------
#  Command macdnd::HandleEnter
# ----------------------------------------------------------------------------
proc macdnd::HandleEnter { path drag_source typelist { data {} } } {
  variable _pressedkeys
  variable _actionlist
  set _pressedkeys 1
  set _actionlist  { copy move link ask private }
  ::tkdnd::generic::SetDroppedData $data
  ::tkdnd::generic::HandleEnter $path $drag_source $typelist $typelist \
           $_actionlist $_pressedkeys
};# macdnd::HandleEnter

# ----------------------------------------------------------------------------
#  Command macdnd::HandlePosition
# ----------------------------------------------------------------------------
proc macdnd::HandlePosition { drop_target rootX rootY {drag_source {}} } {
  variable _pressedkeys
  variable _last_mouse_root_x; set _last_mouse_root_x $rootX
  variable _last_mouse_root_y; set _last_mouse_root_y $rootY
  ::tkdnd::generic::HandlePosition $drop_target $drag_source \
                                   $_pressedkeys $rootX $rootY
};# macdnd::HandlePosition

# ----------------------------------------------------------------------------
#  Command macdnd::HandleLeave
# ----------------------------------------------------------------------------
proc macdnd::HandleLeave { args } {
  ::tkdnd::generic::HandleLeave
};# macdnd::HandleLeave

# ----------------------------------------------------------------------------
#  Command macdnd::HandleDrop
# ----------------------------------------------------------------------------
proc macdnd::HandleDrop { drop_target data args } {
  variable _pressedkeys
  variable _last_mouse_root_x
  variable _last_mouse_root_y
  ## Get the dropped data...
  ::tkdnd::generic::SetDroppedData $data
  ::tkdnd::generic::HandleDrop {} {} $_pressedkeys \
                               $_last_mouse_root_x $_last_mouse_root_y 0
};# macdnd::HandleDrop

# ----------------------------------------------------------------------------
#  Command macdnd::GetDragSourceCommonTypes
# ----------------------------------------------------------------------------
proc macdnd::GetDragSourceCommonTypes { } {
  ::tkdnd::generic::GetDragSourceCommonTypes
};# macdnd::GetDragSourceCommonTypes

# ----------------------------------------------------------------------------
#  Command macdnd::platform_specific_types
# ----------------------------------------------------------------------------
proc macdnd::platform_specific_types { types } {
  ::tkdnd::generic::platform_specific_types $types
}; # macdnd::platform_specific_types

# ----------------------------------------------------------------------------
#  Command macdnd::platform_specific_type
# ----------------------------------------------------------------------------
proc macdnd::platform_specific_type { type } {
  ::tkdnd::generic::platform_specific_type $type
}; # macdnd::platform_specific_type

# ----------------------------------------------------------------------------
#  Command tkdnd::platform_independent_types
# ----------------------------------------------------------------------------
proc ::tkdnd::platform_independent_types { types } {
  ::tkdnd::generic::platform_independent_types $types
}; # tkdnd::platform_independent_types

# ----------------------------------------------------------------------------
#  Command macdnd::platform_independent_type
# ----------------------------------------------------------------------------
proc macdnd::platform_independent_type { type } {
  ::tkdnd::generic::platform_independent_type $type
}; # macdnd::platform_independent_type


================================================
FILE: build_files/tkdnd2.9.2/tkdnd_unix.tcl
================================================
#
# tkdnd_unix.tcl --
#
#    This file implements some utility procedures that are used by the TkDND
#    package.
#
# This software is copyrighted by:
# George Petasis, National Centre for Scientific Research "Demokritos",
# Aghia Paraskevi, Athens, Greece.
# e-mail: petasis@iit.demokritos.gr
#
# The following terms apply to all files associated
# with the software unless explicitly disclaimed in individual files.
#
# The authors hereby grant permission to use, copy, modify, distribute,
# and license this software and its documentation for any purpose, provided
# that existing copyright notices are retained in all copies and that this
# notice is included verbatim in any distributions. No written agreement,
# license, or royalty fee is required for any of the authorized uses.
# Modifications to this software may be copyrighted by their authors
# and need not follow the licensing terms described here, provided that
# the new terms are clearly indicated on the first page of each file where
# they apply.
#
# IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
# ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
# DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
# IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
# NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
# MODIFICATIONS.
#

namespace eval xdnd {
  variable _dragging 0

  proc initialise { } {
    ## Mapping from platform types to TkDND types...
    ::tkdnd::generic::initialise_platform_to_tkdnd_types [list \
       text/plain\;charset=utf-8     DND_Text  \
       UTF8_STRING                   DND_Text  \
       text/plain                    DND_Text  \
       STRING                        DND_Text  \
       TEXT                          DND_Text  \
       COMPOUND_TEXT                 DND_Text  \
       text/uri-list                 DND_Files \
       text/html\;charset=utf-8      DND_HTML  \
       text/html                     DND_HTML  \
       application/x-color           DND_Color \
    ]
  };# initialise

};# namespace xdnd

# ----------------------------------------------------------------------------
#  Command xdnd::HandleXdndEnter
# ----------------------------------------------------------------------------
proc xdnd::HandleXdndEnter { path drag_source typelist time { data {} } } {
  variable _pressedkeys
  variable _actionlist
  variable _typelist
  set _pressedkeys 1
  set _actionlist  { copy move link ask private }
  set _typelist    $typelist
  # puts "xdnd::HandleXdndEnter: $time"
  ::tkdnd::generic::SetDroppedData $data
  ::tkdnd::generic::HandleEnter $path $drag_source $typelist $typelist \
           $_actionlist $_pressedkeys
};# xdnd::HandleXdndEnter

# ----------------------------------------------------------------------------
#  Command xdnd::HandleXdndPosition
# ----------------------------------------------------------------------------
proc xdnd::HandleXdndPosition { drop_target rootX rootY time {drag_source {}} } {
  variable _pressedkeys
  variable _typelist
  variable _last_mouse_root_x; set _last_mouse_root_x $rootX
  variable _last_mouse_root_y; set _last_mouse_root_y $rootY
  # puts "xdnd::HandleXdndPosition: $time"
  ## Get the dropped data...
  catch {
    ::tkdnd::generic::SetDroppedData [GetPositionData $drop_target $_typelist $time]
  }
  ::tkdnd::generic::HandlePosition $drop_target $drag_source \
                                   $_pressedkeys $rootX $rootY
};# xdnd::HandleXdndPosition

# ----------------------------------------------------------------------------
#  Command xdnd::HandleXdndLeave
# ----------------------------------------------------------------------------
proc xdnd::HandleXdndLeave { } {
  ::tkdnd::generic::HandleLeave
};# xdnd::HandleXdndLeave

# ----------------------------------------------------------------------------
#  Command xdnd::_HandleXdndDrop
# ----------------------------------------------------------------------------
proc xdnd::HandleXdndDrop { time } {
  variable _pressedkeys
  variable _last_mouse_root_x
  variable _last_mouse_root_y
  ## Get the dropped data...
  ::tkdnd::generic::SetDroppedData [GetDroppedData \
    [::tkdnd::generic::GetDragSource] [::tkdnd::generic::GetDropTarget] \
    [::tkdnd::generic::GetDragSourceCommonTypes] $time]
  ::tkdnd::generic::HandleDrop {} {} $_pressedkeys \
                               $_last_mouse_root_x $_last_mouse_root_y $time
};# xdnd::HandleXdndDrop

# ----------------------------------------------------------------------------
#  Command xdnd::GetPositionData
# ----------------------------------------------------------------------------
proc xdnd::GetPositionData { drop_target typelist time } {
  foreach {drop_target common_drag_source_types common_drop_target_types} \
    [::tkdnd::generic::FindWindowWithCommonTypes $drop_target $typelist] {break}
  GetDroppedData [::tkdnd::generic::GetDragSource] $drop_target \
    $common_drag_source_types $time
};# xdnd::GetPositionData

# ----------------------------------------------------------------------------
#  Command xdnd::GetDroppedData
# ----------------------------------------------------------------------------
proc xdnd::GetDroppedData { _drag_source _drop_target _common_drag_source_types time } {
  if {![llength $_common_drag_source_types]} {
    error "no common data types between the drag source and drop target widgets"
  }
  ## Is drag source in this application?
  if {[catch {winfo pathname -displayof $_drop_target $_drag_source} p]} {
    set _use_tk_selection 0
  } else {
    set _use_tk_selection 1
  }
  foreach type $_common_drag_source_types {
    # puts "TYPE: $type ($_drop_target)"
    # _get_selection $_drop_target $time $type
    if {$_use_tk_selection} {
      if {![catch {
        selection get -displayof $_drop_target -selection XdndSelection \
                      -type $type
                                              } result options]} {
        return [normalise_data $type $result]
      }
    } else {
      # puts "_selection_get -displayof $_drop_target -selection XdndSelection \
      #                 -type $type -time $time"
      #after 100 [list focus -force $_drop_target]
      #after 50 [list raise [winfo toplevel $_drop_target]]
      if {![catch {
        _selection_get -displayof $_drop_target -selection XdndSelection \
                      -type $type -time $time
                                              } result options]} {
        return [normalise_data $type $result]
      }
    }
  }
  return -options $options $result
};# xdnd::GetDroppedData

# ----------------------------------------------------------------------------
#  Command xdnd::platform_specific_types
# ----------------------------------------------------------------------------
proc xdnd::platform_specific_types { types } {
  ::tkdnd::generic::platform_specific_types $types
}; # xdnd::platform_specific_types

# ----------------------------------------------------------------------------
#  Command xdnd::platform_specific_type
# ----------------------------------------------------------------------------
proc xdnd::platform_specific_type { type } {
  ::tkdnd::generic::platform_specific_type $type
}; # xdnd::platform_specific_type

# ----------------------------------------------------------------------------
#  Command tkdnd::platform_independent_types
# ----------------------------------------------------------------------------
proc ::tkdnd::platform_independent_types { types } {
  ::tkdnd::generic::platform_independent_types $types
}; # tkdnd::platform_independent_types

# ----------------------------------------------------------------------------
#  Command xdnd::platform_independent_type
# ----------------------------------------------------------------------------
proc xdnd::platform_independent_type { type } {
  ::tkdnd::generic::platform_independent_type $type
}; # xdnd::platform_independent_type

# ----------------------------------------------------------------------------
#  Command xdnd::_normalise_data
# ----------------------------------------------------------------------------
proc xdnd::normalise_data { type data } {
  # Tk knows how to interpret the following types:
  #    STRING, TEXT, COMPOUND_TEXT
  #    UTF8_STRING
  # Else, it returns a list of 8 or 32 bit numbers...
  switch -glob $type {
    STRING - UTF8_STRING - TEXT - COMPOUND_TEXT {return $data}
    text/html {
      if {[catch {
            encoding convertfrom unicode $data
           } string]} {
        set string $data
      }
      return [string map {\r\n \n} $string]
    }
    text/html\;charset=utf-8  -
    text/plain\;charset=utf-8 -
    text/plain {
      if {[catch {
            encoding convertfrom utf-8 [tkdnd::bytes_to_string $data]
           } string]} {
        set string $data
      }
      return [string map {\r\n \n} $string]
    }
    text/uri-list* {
      if {[catch {
            encoding convertfrom utf-8 [tkdnd::bytes_to_string $data]
          } string]} {
        set string $data
      }
      ## Get rid of \r\n
      set string [string trim [string map {\r\n \n} $string]]
      set files {}
      foreach quoted_file [split $string] {
        set file [tkdnd::urn_unquote $quoted_file]
        switch -glob $file {
          \#*       {}
          file://*  {lappend files [string range $file 7 end]}
          ftp://*   -
          https://* -
          http://*  {lappend files $quoted_file}
          default   {lappend files $file}
        }
      }
      return $files
    }
    application/x-color {
      return $data
    }
    text/x-moz-url -
    application/q-iconlist -
    default    {return $data}
  }
}; # xdnd::normalise_data

#############################################################################
##
##  XDND drag implementation
##
#############################################################################

# ----------------------------------------------------------------------------
#  Command xdnd::_selection_ownership_lost
# ----------------------------------------------------------------------------
proc xdnd::_selection_ownership_lost {} {
  variable _dragging
  set _dragging 0
};# _selection_ownership_lost

# ----------------------------------------------------------------------------
#  Command xdnd::_dodragdrop
# ----------------------------------------------------------------------------
proc xdnd::_dodragdrop { source actions types data button } {
  variable _dragging

  # p
Download .txt
gitextract_zrp3by6s/

├── .dockerignore
├── .github/
│   └── workflows/
│       ├── build.yml
│       └── winget.yml
├── .gitignore
├── CHANGELOG.txt
├── Dockerfile
├── LICENSE
├── README.md
├── build.cmd
├── build.py
├── build_files/
│   ├── TkinterDnD2/
│   │   ├── TkinterDnD.py
│   │   └── __init__.py
│   ├── Updater.cs.txt
│   ├── Updater.exe.MANIFEST
│   ├── daemon.spec
│   ├── flatpak-pip-generator.py
│   ├── mc_version_info.txt
│   ├── mcu_version_info.txt
│   ├── onedir.spec
│   ├── portable.spec
│   ├── pyaudio-0.2.14-cp314-cp314-win_amd64.whl
│   ├── pyinstaller-6.16.0-py3-none-any.whl
│   ├── setup_script.iss
│   ├── tkdnd2.9.2/
│   │   ├── pkgIndex.tcl
│   │   ├── tkdnd.tcl
│   │   ├── tkdnd2.9.2.lib
│   │   ├── tkdnd_compat.tcl
│   │   ├── tkdnd_generic.tcl
│   │   ├── tkdnd_macosx.tcl
│   │   ├── tkdnd_unix.tcl
│   │   ├── tkdnd_utils.tcl
│   │   └── tkdnd_windows.tcl
│   └── updater.spec
├── conftest.py
├── linux_install.py
├── linux_install.sh
├── music_caster.desktop
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── resources/
│   ├── Music Caster Icon.psd
│   ├── assets.psd
│   ├── favicons/
│   │   ├── browserconfig.xml
│   │   └── site.webmanifest
│   ├── gude-2023-11-11.log
│   └── icons/
│       └── icon.icns
├── scripts/
│   ├── arch-install.sh
│   ├── debian-install.sh
│   ├── fedora-install.sh
│   ├── pre-req.sh
│   └── suse-install.sh
└── src/
    ├── audio_player.py
    ├── b64_images.py
    ├── ca.elijahlopez.MusicCaster.yml
    ├── experiments.py
    ├── go.mod
    ├── go.sum
    ├── gui/
    │   ├── __init__.py
    │   ├── components.py
    │   └── views.py
    ├── knownpaths.py
    ├── languages/
    │   ├── da.txt
    │   ├── de.txt
    │   ├── en.txt
    │   ├── es.txt
    │   ├── fr.txt
    │   ├── it.txt
    │   ├── nl.txt
    │   ├── pt-br.txt
    │   ├── ru.txt
    │   ├── sk.txt
    │   └── uk.txt
    ├── meta.py
    ├── modules/
    │   ├── db.py
    │   ├── error_reporting.py
    │   ├── iph1papi.py
    │   ├── playing_status.py
    │   ├── resolution_switcher.py
    │   ├── url_metadata.py
    │   └── win32_media_controls.py
    ├── music_caster.bat
    ├── music_caster.py
    ├── pyoxidizer.bzl
    ├── shared.py
    ├── static/
    │   └── style.css
    ├── sys_tray.py
    ├── templates/
    │   └── index.html
    ├── test_cases/
    │   └── ipconfig.py
    ├── test_harness.py
    ├── theme/
    │   ├── LICENSE
    │   ├── dark.tcl
    │   ├── light.tcl
    │   └── sun-valley.tcl
    ├── updater.go
    ├── utils.py
    ├── vlc_lib/
    │   ├── libvlc.so.5.6.0
    │   └── libvlccore.so.9.0.0
    └── webview_demo.py
Download .txt
SYMBOL INDEX (455 symbols across 23 files)

FILE: build.py
  class ProgressUpload (line 48) | class ProgressUpload:
    method __init__ (line 50) | def __init__(self, filename, chunk_size=1_024_000):
    method __iter__ (line 60) | def __iter__(self):
    method __len__ (line 79) | def __len__(self):
  function read_env (line 83) | def read_env(env_file='.env'):
  function add_new_changes (line 94) | def add_new_changes(prev_changes: str):
  function set_spec_debug (line 120) | def set_spec_debug(debug_option):
  function create_zip (line 132) | def create_zip(zip_filename, files_to_zip, compression=zipfile.ZIP_BZIP2):
  function update_versions (line 144) | def update_versions(version):
  function local_install (line 182) | def local_install():
  function test (line 202) | def test(title, fn, assert_statement=False):
  function upgrade_yt_dlp (line 216) | def upgrade_yt_dlp():

FILE: build_files/TkinterDnD2/TkinterDnD.py
  function _require (line 36) | def _require(tkroot):
  class DnDEvent (line 46) | class DnDEvent:
  class DnDWrapper (line 74) | class DnDWrapper:
    method _substitute_dnd (line 85) | def _substitute_dnd(self, *args):
    method _dnd_bind (line 137) | def _dnd_bind(self, what, sequence, func, add, needcleanup=True):
    method dnd_bind (line 157) | def dnd_bind(self, sequence=None, func=None, add=None):
    method drag_source_register (line 175) | def drag_source_register(self, button=None, *dndtypes):
    method drag_source_unregister (line 206) | def drag_source_unregister(self):
    method drop_target_register (line 215) | def drop_target_register(self, *dndtypes):
    method drop_target_unregister (line 229) | def drop_target_unregister(self):
    method platform_independent_types (line 238) | def platform_independent_types(self, *dndtypes):
    method platform_specific_types (line 249) | def platform_specific_types(self, *dndtypes):
    method get_dropfile_tempdir (line 260) | def get_dropfile_tempdir(self):
    method set_dropfile_tempdir (line 271) | def set_dropfile_tempdir(self, tempdir):
  class Tk (line 281) | class Tk(tkinter.Tk, DnDWrapper):
    method __init__ (line 285) | def __init__(self, *args, **kw):
  class TixTk (line 290) | class TixTk(tkinter.Tk, DnDWrapper):
    method __init__ (line 294) | def __init__(self, *args, **kw):

FILE: build_files/flatpak-pip-generator.py
  function get_pypi_url (line 55) | def get_pypi_url(name: str, filename: str) -> str:
  function get_tar_package_url_pypi (line 67) | def get_tar_package_url_pypi(name: str, version: str) -> str:
  function get_package_name (line 79) | def get_package_name(filename: str) -> str:
  function get_file_version (line 101) | def get_file_version(filename: str) -> str:
  function get_file_hash (line 110) | def get_file_hash(filename: str) -> str:
  function download_tar_pypi (line 122) | def download_tar_pypi(url: str, tempdir: str) -> None:
  function parse_continuation_lines (line 129) | def parse_continuation_lines(fin):
  function fprint (line 140) | def fprint(string: str) -> None:
  class OrderedDumper (line 443) | class OrderedDumper(yaml.Dumper):
    method increase_indent (line 444) | def increase_indent(self, flow=False, indentless=False):
  function dict_representer (line 447) | def dict_representer(dumper, data):

FILE: conftest.py
  function pytest_addoption (line 4) | def pytest_addoption(parser):
  function ci (line 11) | def ci(pytestconfig):
  function pytest_collection_modifyitems (line 15) | def pytest_collection_modifyitems(config, items):

FILE: src/audio_player.py
  class AudioPlayerUnit (line 32) | class AudioPlayerUnit(IntEnum):
  class AudioPlayer (line 37) | class AudioPlayer:
    method __init__ (line 40) | def __init__(self, skip_vlc=False):
    method has_media (line 46) | def has_media(self):
    method is_busy (line 49) | def is_busy(self):
    method play (line 53) | def play(self, media_path, start_playing=True, volume=None, start_from...
    method load (line 72) | def load(self, file_path):
    method pause (line 75) | def pause(self):
    method resume (line 84) | def resume(self):
    method stop (line 99) | def stop(self):
    method percent_to_db_percent (line 109) | def percent_to_db_percent(percent: float):
    method db_percent_to_percent (line 119) | def db_percent_to_percent(db: float):
    method set_volume (line 123) | def set_volume(self, volume):
    method get_volume (line 130) | def get_volume(self):
    method set_pos (line 137) | def set_pos(self, position, unit=AudioPlayerUnit.SECOND):
    method get_pos (line 141) | def get_pos(self, unit=AudioPlayerUnit.SECOND) -> float:
    method is_playing (line 148) | def is_playing(self):
    method is_paused (line 152) | def is_paused(self):
    method is_idle (line 155) | def is_idle(self):
    method toggle_mute (line 159) | def toggle_mute(self):
    method mute (line 162) | def mute(self):
    method unmute (line 165) | def unmute(self):
    method get_length (line 168) | def get_length(self, unit=AudioPlayerUnit.SECOND) -> float:
    method get_sample_rate (line 171) | def get_sample_rate(self):

FILE: src/experiments.py
  function get_audio_wave (line 15) | def get_audio_wave(file):

FILE: src/gui/__init__.py
  function focus_window (line 11) | def focus_window(window: Sg.Window, is_frozen=getattr(sys, 'frozen', Fal...
  function window_is_foreground (line 31) | def window_is_foreground(window: Sg.Window):

FILE: src/gui/components.py
  function get_styled_button_font (line 11) | def get_styled_button_font():
  function StyledButton (line 16) | def StyledButton(button_text, fill, text_color, tooltip=None, key=None, ...
  function IconButton (line 48) | def IconButton(image_data, key, tooltip, bg):
  function Checkbox (line 52) | def Checkbox(name, key, settings, on_right=False, tooltip=None):
  function QRCode (line 62) | def QRCode(text_to_encode):

FILE: src/gui/views.py
  class GuiContext (line 61) | class GuiContext:
    method update (line 68) | def update(cls, text_color, background_colour, accent_color, experimen...
  function MiniPlayerWindow (line 75) | def MiniPlayerWindow(playing_status, settings, title: str, artist: str, ...
  function MainWindow (line 90) | def MainWindow(playing_status, settings, title: str, artist: str, album:...
  function MusicControls (line 145) | def MusicControls(settings, playing_status: PlayingStatus, prev_button_p...
  function ProgressBar (line 166) | def ProgressBar(settings, track_position, track_length, playing_status: ...
  function URLTab (line 190) | def URLTab(accent_color, bg):
  function QueueTab (line 201) | def QueueTab(queue, listbox_selected, listbox_height):
  function LibraryTab (line 243) | def LibraryTab(music_lib, listbox_height, alternate_bg, vertical_gui: bo...
  function PlaylistsTab (line 267) | def PlaylistsTab(playlists, vertical_gui: bool, show_album_art: bool):
  function TimerTab (line 317) | def TimerTab(timer, is_shut_down: bool, is_hibernate: bool, is_sleep: bo...
  function MetadataTab (line 343) | def MetadataTab():
  function SettingsTab (line 361) | def SettingsTab(settings, web_ui_url):
  function VideoTab (line 441) | def VideoTab(devices):

FILE: src/knownpaths.py
  class KNOWN_FOLDER_FLAG (line 37) | class KNOWN_FOLDER_FLAG:
  class GUID (line 56) | class GUID(ctypes.Structure):  # [1]
    method __init__ (line 64) | def __init__(self, uuid_):
  class FOLDERID (line 78) | class FOLDERID:  # [2]
  class UserHandle (line 175) | class UserHandle:  # [3]
  class PathNotFoundException (line 193) | class PathNotFoundException(Exception):
  function sh_get_known_folder_path (line 197) | def sh_get_known_folder_path(folderid, user_handle=UserHandle.current, f...

FILE: src/meta.py
  class State (line 79) | class State:

FILE: src/modules/db.py
  class DatabaseConnection (line 11) | class DatabaseConnection:
    method create_connection (line 17) | def create_connection():
    method __init__ (line 22) | def __init__(self, db_override=None):
    method __enter__ (line 26) | def __enter__(self):
    method __exit__ (line 30) | def __exit__(self, exc_type, exc_val, exc_tb):
  function init_db (line 89) | def init_db():

FILE: src/modules/iph1papi.py
  class NET_LUID (line 15) | class NET_LUID(Structure):
  class _MIB_IF_ROW2 (line 34) | class _MIB_IF_ROW2(Structure):
  class _MIB_IF_TABLE2 (line 86) | class _MIB_IF_TABLE2(Structure):
  function get_if_table2 (line 98) | def get_if_table2():
  class MIB_IPINTERFACE_ROW (line 120) | class MIB_IPINTERFACE_ROW(Structure):
  class _MIB_IPINTERFACE_TABLE (line 160) | class _MIB_IPINTERFACE_TABLE(Structure):
  function get_ip_interface_table (line 169) | def get_ip_interface_table():
  class IP_ADDRESS_STRING (line 195) | class IP_ADDRESS_STRING(Structure):
  class IP_MASK_STRING (line 201) | class IP_MASK_STRING(Structure):
  class IP_ADDR_STRING (line 207) | class IP_ADDR_STRING(Structure):
  class IP_ADAPTER_INFO (line 219) | class IP_ADAPTER_INFO(Structure):
  function get_adapters_info (line 254) | def get_adapters_info():

FILE: src/modules/playing_status.py
  class PlayingStatus (line 4) | class PlayingStatus:
    method __init__ (line 19) | def __init__(self):
    method busy (line 27) | def busy(self):
    method stopped (line 31) | def stopped(self):
    method playing (line 35) | def playing(self):
    method paused (line 39) | def paused(self):
    method stop (line 42) | def stop(self):
    method play (line 45) | def play(self, device_is_local: bool = True):
    method play_uri (line 48) | def play_uri(self, position, track_length, device_is_local: bool):
    method pause (line 56) | def pause(self):
    method play_system_audio (line 59) | def play_system_audio(self):
    method __repr__ (line 64) | def __repr__(self):
    method __eq__ (line 67) | def __eq__(self, other):

FILE: src/modules/resolution_switcher.py
  class SYSTEM_POWER_STATUS (line 24) | class SYSTEM_POWER_STATUS(ctypes.Structure):
  function is_plugged_in (line 41) | def is_plugged_in(throw_error=True):
  function get_aspect_ratio (line 56) | def get_aspect_ratio(width, height):
  function get_current_res (line 60) | def get_current_res(w=None, h=None):
  function get_initial_res (line 74) | def get_initial_res():
  function get_initial_dpi_scale (line 85) | def get_initial_dpi_scale():
  function get_all_refresh_rates (line 95) | def get_all_refresh_rates():
  function get_all_resolutions (line 108) | def get_all_resolutions():
  function get_recommended_dpi_idx (line 146) | def get_recommended_dpi_idx():
  function calc_dpi_scale (line 153) | def calc_dpi_scale(new_w, _):
  function set_resolution (line 162) | def set_resolution(width: int, height: int, dpi_scale: int, refresh_rate...
  function set_res_curry (line 186) | def set_res_curry(width, height, dpi_scale):
  function fmt_res (line 191) | def fmt_res(width, height, show_width=False):
  function on_exit (line 196) | def on_exit():

FILE: src/modules/url_metadata.py
  function tbr_audio_key (line 16) | def tbr_audio_key(item):
  function tbr_video_key (line 19) | def tbr_video_key(item):
  function ydl_get_metadata (line 22) | def ydl_get_metadata(item, duration_helper=True):
  class URLMetadata (line 68) | class URLMetadata:
    method __init__ (line 77) | def __init__(self, src: str, url_type: str, title: str, artist: str, a...
    method __hash__ (line 98) | def __hash__(self) -> int:
    method __getitem__ (line 101) | def __getitem__(self, key):
    method __setitem__ (line 111) | def __setitem__(self, key, value):
    method __delitem__ (line 122) | def __delitem__(self, key):
    method __iter__ (line 128) | def __iter__(self):
    method __len__ (line 131) | def __len__(self):
    method keys (line 134) | def keys(self):
    method values (line 137) | def values(self):
    method items (line 140) | def items(self):
    method get (line 143) | def get(self, key, default=None):
    method save_to_db (line 148) | def save_to_db(self, cur):
    method from_db (line 174) | def from_db(cls, conn, url) -> Self | None:
    method from_dict (line 203) | def from_dict(cls, data):
    method hash (line 210) | def hash(self) -> str:
    method image_cache_path (line 214) | def image_cache_path(self):
    method is_expired (line 218) | def is_expired(self):
    method get_cover_image (line 223) | def get_cover_image(self) -> bytes:

FILE: src/modules/win32_media_controls.py
  class SystemMediaTransportControlsButton (line 9) | class SystemMediaTransportControlsButton(enum.IntEnum):
  class SystemMediaControls (line 22) | class SystemMediaControls:
    method __init__ (line 23) | def __init__(self, on_event):
    method _on_btn_press (line 43) | def _on_btn_press(self, sender, args: media.SystemMediaTransportContro...
    method set_source (line 46) | def set_source(self, source):
    method set_playing (line 54) | def set_playing(self):
    method set_paused (line 59) | def set_paused(self):
    method set_stopped (line 64) | def set_stopped(self):
    method set_closed (line 67) | def set_closed(self):
    method set_metadata (line 72) | def set_metadata(self, title, artist, album, thumb_uri: str):
    method update_time (line 89) | def update_time(self):

FILE: src/music_caster.py
  function create_pid_file (line 43) | def create_pid_file(port=None):
  function parse_pid_file (line 50) | def parse_pid_file():
  function ensure_single_instance (line 62) | def ensure_single_instance(debugging=False):
  function json_dumps (line 168) | def json_dumps(d):
  function activate_instance (line 171) | def activate_instance(port=2001, default_timeout=0.5, to_port=2004):
  function get_downloads_folder (line 407) | def get_downloads_folder():
  function get_installer_path (line 416) | def get_installer_path():
  function get_default_music_folder (line 423) | def get_default_music_folder():
  function get_line_number (line 461) | def get_line_number():
  function tray_notify (line 466) | def tray_notify(message, title='Music Caster', context=''):
  function close_tray (line 473) | def close_tray():
  function save_settings (line 478) | def save_settings():
  function is_debug (line 506) | def is_debug():
  function refresh_tray (line 510) | def refresh_tray(refresh_devices=False):
  function refresh_tray_icon (line 576) | def refresh_tray_icon():
  function update_settings (line 581) | def update_settings(settings_key, new_value):
  function save_queues (line 596) | def save_queues():
  function update_volume (line 610) | def update_volume(new_vol, _from=''):
  function cycle_repeat (line 629) | def cycle_repeat():
  function create_support_email_url (line 636) | def create_support_email_url():
  function handle_exception (line 648) | def handle_exception(e: Exception, restart_program=False) -> bool:
  function get_current_art (line 696) | def get_current_art() -> bytes:
  function get_metadata_wrapped (line 719) | def get_metadata_wrapped(file_path: str) -> dict:  # keys: title, artist...
  function get_uri_metadata (line 734) | def get_uri_metadata(uri, read_file=True):
  function get_current_metadata (line 762) | def get_current_metadata() -> dict | URLMetadata:
  function get_audio_uris (line 770) | def get_audio_uris(uris: Iterable, scan_uris=True, ignore_m3u=False, par...
  function index_all_tracks (line 809) | def index_all_tracks(update_global=True, ignore_files: set | None = None...
  function download (line 878) | def download(url, outfile):
  function load_settings (line 890) | def load_settings(first_load=False):  # up to 0.4 seconds
  function page_not_found (line 977) | def page_not_found(_):
  function upload_files (line 982) | def upload_files():  # web GUI
  function get_request_data (line 994) | def get_request_data():
  function web_action (line 1001) | def web_action(command):
  function web_index (line 1048) | def web_index():  # web GUI
  function api_state (line 1104) | def api_state():
  function api_play (line 1115) | def api_play():
  function handle_500 (line 1165) | def handle_500(_e):
  function api_get_debug_info (line 1179) | def api_get_debug_info():
  function api_running (line 1190) | def api_running():
  function api_exit (line 1200) | def api_exit():
  function api_change_setting (line 1206) | def api_change_setting():
  function api_refresh_devices (line 1227) | def api_refresh_devices():
  function api_rescan_library (line 1233) | def api_rescan_library():
  function api_get_devices (line 1239) | def api_get_devices():
  function api_change_device (line 1253) | def api_change_device(_uuid):
  function cancel_timer (line 1257) | def cancel_timer():
  function set_timer (line 1263) | def set_timer(val):
  function api_set_timer (line 1292) | def api_set_timer():
  function get_cover_jpg_data (line 1304) | def get_cover_jpg_data(file_path) -> io.BytesIO:
  function report_album_art_buffer_error (line 1314) | def report_album_art_buffer_error(file_path: str):
  function api_get_file (line 1323) | def api_get_file():
  function api_get_dz (line 1340) | def api_get_dz():
  function api_system_audio (line 1378) | def api_system_audio(get_thumb=''):
  function cast_try_reconnect (line 1388) | def cast_try_reconnect(switch_twice=False):
  function cast_info_sorter (line 1411) | def cast_info_sorter(ci1: CastInfo, ci2: CastInfo):
  function get_devices (line 1426) | def get_devices():
  class UpdateFailed (line 1433) | class UpdateFailed(Exception):
  class StatusCastListener (line 1437) | class StatusCastListener(CastStatusListener):
    method __init__ (line 1440) | def __init__(self, _cast):
    method new_cast_status (line 1444) | def new_cast_status(self, status):
  class MediaCastListener (line 1448) | class MediaCastListener(MediaStatusListener):
    method __init__ (line 1449) | def __init__(self, _cast):
    method new_media_status (line 1453) | def new_media_status(self, status):
    method load_media_failed (line 1456) | def load_media_failed(self, item, error_code):
  class MyCastListener (line 1459) | class MyCastListener(pychromecast.discovery.AbstractCastListener):
    method add_cast (line 1461) | def add_cast(self, uuid, _service: str):
    method remove_cast (line 1482) | def remove_cast(self, uuid, _service: str, cast_info):
    method update_cast (line 1490) | def update_cast(self, uuid, _service: str):
  function get_device (line 1507) | def get_device(device_uuid):
  function change_device (line 1512) | def change_device(new_uuid='local', unresponsive_cast=False):
  function un_shuffle_queue (line 1580) | def un_shuffle_queue():
  function shuffle_queue (line 1604) | def shuffle_queue():
  function format_pl_lb (line 1624) | def format_pl_lb(tracks):
  function format_uri (line 1650) | def format_uri(uri: str, use_basename=False, _for=''):
  function create_track_list (line 1689) | def create_track_list():
  function update_gui (line 1716) | def update_gui():
  function after_play (line 1763) | def after_play(title, artists: str, album, autoplay, switching_device):
  function play_system_audio (line 1803) | def play_system_audio(switching_device=False, show_error=False):
  function url_expired (line 1853) | def url_expired(uri):
  function get_url_metadata (line 1861) | def get_url_metadata(url, fetch_art=True) -> list[dict | URLMetadata]:
  function play_url (line 2098) | def play_url(position=0, autoplay=True, switching_device=False, show_err...
  function play (line 2174) | def play(position=0, autoplay=True, switching_device=False, show_error=F...
  function metadata_key (line 2293) | def metadata_key(filename, album_sort=True):
  function play_uris (line 2303) | def play_uris(uris: Iterable, return_if_empty=True, queue_uris=False,
  function play_all (line 2386) | def play_all(starting_files: Iterable = None, queue_only=False):
  function queue_all (line 2412) | def queue_all():
  function open_dialog (line 2417) | def open_dialog(title, for_dir=False, filetypes=None, single_file=False):
  function file_action (line 2443) | def file_action(action='pf'):
  function video_file_action (line 2465) | def video_file_action():
  function folder_action (line 2480) | def folder_action(action='pf'):
  function get_track_position (line 2505) | def get_track_position():
  function pause (line 2515) | def pause(source=''):
  function resume (line 2566) | def resume(source=''):
  function stop (line 2608) | def stop(stopped_from: str, stop_cast=True):
  function set_pos (line 2646) | def set_pos(new_position: int):
  function next_track (line 2699) | def next_track(from_timeout=False, times=1, forced=False, ignore_timesta...
  function prev_track (line 2770) | def prev_track(times=1, forced=False, ignore_timestamps=False):
  class UpdateChecker (line 2796) | class UpdateChecker(threading.Timer):
    method __init__ (line 2801) | def __init__(self):
    method run (line 2807) | def run(self):
    method check_for_updates (line 2811) | def check_for_updates(self):
    method auto_update (line 2823) | def auto_update(self, install_update=True, from_gui=False):
  function background_thread (line 2897) | def background_thread():
  function on_smtc_btn_press (line 2952) | def on_smtc_btn_press(event: SystemMediaTransportControlsButton):
  function on_press (line 2964) | def on_press(key):
  function get_window_location (line 2986) | def get_window_location():
  function metadata_process_file (line 3003) | def metadata_process_file(file: None | os.PathLike, callback_source):
  function add_music_folder (line 3033) | def add_music_folder(folders):
  function set_callbacks (line 3046) | def set_callbacks():
  function activate_gui (line 3158) | def activate_gui(selected_tab=None, url_option='url_play'):
  function uri_at_idx (line 3254) | def uri_at_idx(idx=0, offset=None):
  function locate_uri (line 3267) | def locate_uri(selected_track_index=None, uri=None):
  function exit_program (line 3297) | def exit_program(quick_exit=False):
  function playlist_action (line 3325) | def playlist_action(playlist_name, action='play'):
  function other_tray_actions (line 3341) | def other_tray_actions(_tray_item):
  function event_is_close (line 3355) | def event_is_close(main_event, main_values):
  function update_playlist_ui (line 3360) | def update_playlist_ui(set_to_index=None):
  function read_main_window (line 3366) | def read_main_window():
  function start_on_login_modifications (line 4222) | def start_on_login_modifications():
  function cast_monitor (line 4235) | def cast_monitor(sent: bool = True, msg: dict | None = None, is_callback...
  function handle_action (line 4323) | def handle_action(action):

FILE: src/shared.py
  function get_running_processes (line 9) | def get_running_processes(look_for='', pid=None, add_exe=True):
  function is_already_running (line 48) | def is_already_running(look_for='Music Caster', threshold=1, pid=None) -...

FILE: src/sys_tray.py
  function system_tray (line 13) | def system_tray(main_queue: mp.Queue, child_queue: mp.Queue):

FILE: src/test_harness.py
  function test_get_running_processes (line 192) | def test_get_running_processes():
  function test_get_file_name (line 201) | def test_get_file_name(file_path, expected):
  function test_display_lang (line 205) | def test_display_lang():
  function test_internationalization (line 211) | def test_internationalization():
  function test_get_lang_pack (line 220) | def test_get_lang_pack(code):
  function test_get_translation (line 230) | def test_get_translation(code):
  function test_valid_audio_file (line 256) | def test_valid_audio_file(ext):
  function test_audio_length (line 267) | def test_audio_length(file):
  function test_audio_length_fail (line 276) | def test_audio_length_fail(file):
  function test_default_output_device (line 287) | def test_default_output_device():
  function test_natural_sort (line 300) | def test_natural_sort(unsorted, expected):
  function test_valid_color_code (line 319) | def test_valid_color_code(color_code):
  function test_invalid_color_codes (line 339) | def test_invalid_color_codes(color_code):
  function test_get_metadata (line 348) | def test_get_metadata(file, expected, expected_first_artist):
  function test_ipv4 (line 358) | def test_ipv4():
  function test_ipv6 (line 362) | def test_ipv6():
  function test_mac (line 366) | def test_mac():
  function test_ipv4_wifi_match (line 370) | def test_ipv4_wifi_match():
  function test_ipv4_general_match (line 377) | def test_ipv4_general_match():
  function test_better_shuffle (line 388) | def test_better_shuffle():
  function test_is_already_running (line 396) | def test_is_already_running():
  function test_yt_id (line 414) | def test_yt_id(url, expected_id):
  function test_custom_art (line 418) | def test_custom_art():
  function test_album_art (line 423) | def test_album_art(file):
  function test_repeat_img_tooltip (line 436) | def test_repeat_img_tooltip(option, expected_img, expected_label):
  function test_resize_img (line 441) | def test_resize_img(size):
  function test_spotify (line 458) | def test_spotify(url):
  function test_deezer (line 483) | def test_deezer(url):
  function running_in_ci (line 495) | def running_in_ci(request):
  function test_ydl (line 504) | def test_ydl(running_in_ci, url):
  function test_get_proxies (line 533) | def test_get_proxies():
  function test_fix_path (line 541) | def test_fix_path(path, expected):
  function test_fix_path_win32 (line 552) | def test_fix_path_win32(path, expected):
  function test_progress_bar_texts (line 570) | def test_progress_bar_texts(position, length, expected):
  function test_export_playlist (line 574) | def test_export_playlist():
  function test_youtube_comments (line 584) | def test_youtube_comments(url):
  function uploading_after (line 590) | def uploading_after(request):
  function test_auto_update (line 595) | def test_auto_update(request):
  function test_get_latest_release (line 599) | def test_get_latest_release(uploading_after, test_auto_update):
  function test_database (line 613) | def test_database():

FILE: src/updater.go
  function loadSettings (line 18) | func loadSettings() map[string]interface{} {
  function main (line 31) | func main() {
  function extractZip (line 93) | func extractZip(src string) error {
  function download (line 153) | func download(url string, filepath string) {

FILE: src/utils.py
  class SystemAudioRecorder (line 72) | class SystemAudioRecorder:
    method __init__ (line 76) | def __init__(self):
    method get_audio_data (line 86) | def get_audio_data(self, delay=0):
    method _start_recording (line 115) | def _start_recording(self):
    method create_stream (line 129) | def create_stream(self, output_device):
    method get_wav_header (line 142) | def get_wav_header(self):
    method stop (line 159) | def stop(self):
    method start (line 162) | def start(self):
  class InvalidAudioFile (line 173) | class InvalidAudioFile(Exception):
  class Unknown (line 177) | class Unknown(str):
    method __new__ (line 180) | def __new__(cls, _property):
    method __repr__ (line 185) | def __repr__(self):
    method __str__ (line 188) | def __str__(self):
    method __lt__ (line 191) | def __lt__(self, other):
    method __le__ (line 194) | def __le__(self, other):
    method __gt__ (line 197) | def __gt__(self, other):
    method __ge__ (line 200) | def __ge__(self, other):
    method __eq__ (line 203) | def __eq__(self, other):
    method __ne__ (line 206) | def __ne__(self, other):
    method split (line 209) | def split(self, *args, **kwargs):
    method __len__ (line 212) | def __len__(self):
  function exception_wrapper (line 216) | def exception_wrapper(f):
  class DiscordPresence (line 226) | class DiscordPresence:
    method init_rpc (line 235) | def init_rpc(cls):
    method connect (line 241) | def connect(cls, confirm_connect=True):
    method update (line 248) | def update(cls, state: str, details: str, large_text: str, end: int = 0,
    method clear (line 257) | def clear(cls, confirm=True):
    method close (line 263) | def close(cls):
  class Device (line 272) | class Device:
    method __init__ (line 275) | def __init__(self, cast_info_or_none=None):
    method id (line 281) | def id(self):
    method LOCAL_DEVICE (line 285) | def LOCAL_DEVICE(cls):
    method name (line 289) | def name(self):
    method as_tray_name (line 294) | def as_tray_name(self, active_id):
    method tray_key (line 300) | def tray_key(self):
    method gui_key (line 304) | def gui_key(self):
    method as_tray_item (line 307) | def as_tray_item(self, active_id) -> tuple:
    method __eq__ (line 310) | def __eq__(self, other):
    method __str__ (line 313) | def __str__(self):
    method __repr__ (line 316) | def __repr__(self):
  function get_file_name (line 320) | def get_file_name(file_path): return Path(file_path).stem
  function timing (line 324) | def timing(f):
  function time_cache (line 334) | def time_cache(max_age, maxsize=None, typed=False):
  function get_languages (line 362) | def get_languages():
  function get_lang_pack (line 367) | def get_lang_pack(lang):
  function get_display_lang (line 384) | def get_display_lang():
  function log_translation_error (line 393) | def log_translation_error(string, lang):
  function get_translation (line 398) | def get_translation(string, lang='', as_title=False):
  function t (line 414) | def t(string, as_title=False):
  function natural_key_file (line 418) | def natural_key_file(filename):
  function valid_color_code (line 424) | def valid_color_code(code):
  function get_audio_length (line 429) | def get_audio_length(file_path) -> int:
  function valid_audio_file (line 455) | def valid_audio_file(uri) -> bool:
  function set_metadata (line 463) | def set_metadata(file_path: str, metadata: dict):
  function get_metadata (line 566) | def get_metadata(file_path: str):
  function open_in_browser (line 645) | def open_in_browser(url):
  function get_album_art (line 651) | def get_album_art(file_path: str, folder_cover_override=False) -> Tuple[...
  function fix_path (line 693) | def fix_path(path, by_os=True): return str(Path(path)) if by_os else pat...
  function get_first_artist (line 696) | def get_first_artist(artists: str) -> str: return artists.split(', ', 1)[0]
  function get_ipv6 (line 699) | def get_ipv6():
  function clean_ipconfig (line 724) | def clean_ipconfig(ipconfig_raw):
  function get_ipv4 (line 738) | def get_ipv4():
  function get_lan_ip (line 760) | def get_lan_ip() -> str:
  function get_mac (line 764) | def get_mac(): return ':'.join(['{:02x}'.format((getnode() >> ele) & 0xf...
  function better_shuffle (line 767) | def better_shuffle(seq, first=0, last=-1):
  function dz (line 784) | def dz():
  function ydl (line 790) | def ydl(proxy=None, quiet=False):
  function ydl_extract_info (line 802) | def ydl_extract_info(url, quiet=False):
  function get_yt_id (line 816) | def get_yt_id(url, ignore_playlist=False):
  function get_yt_urls (line 834) | def get_yt_urls(video_id):
  function is_os_64bit (line 847) | def is_os_64bit(): return platform.machine().endswith('64')
  function delete_sub_key (line 850) | def delete_sub_key(root, current_key):
  function add_reg_handlers (line 865) | def add_reg_handlers(path_to_exe, add_folder_context=True):
  function get_default_output_device (line 962) | def get_default_output_device():
  function resize_img (line 987) | def resize_img(base64data: bytes, bg, new_size=COVER_NORMAL, default_art...
  function export_playlist (line 1018) | def export_playlist(playlist_name, uris):
  function parse_m3u (line 1032) | def parse_m3u(playlist_file):
  function get_latest_release (line 1042) | def get_latest_release(ver, this_version, force=False):
  function get_proxies (line 1065) | def get_proxies(add_local=True):
  function get_proxy (line 1097) | def get_proxy(add_local=True):
  function get_spotify_headers (line 1103) | def get_spotify_headers():
  function search_album_art_spotify (line 1111) | def search_album_art_spotify(title, artist, mkt):
  function parse_spotify_track (line 1124) | def parse_spotify_track(track_obj, parent_url='') -> dict:
  function get_spotify_track (line 1150) | def get_spotify_track(url):
  function get_spotify_album (line 1161) | def get_spotify_album(url):
  function get_spotify_playlist (line 1168) | def get_spotify_playlist(url):
  function get_spotify_tracks (line 1179) | def get_spotify_tracks(url):
  function get_cookies (line 1193) | def get_cookies(domain_contains, cookie_name='', return_first=True, retu...
  function parse_deezer_page (line 1218) | def parse_deezer_page(url):
  function parse_deezer_track (line 1236) | def parse_deezer_track(track_obj) -> dict:
  function set_dz_url (line 1275) | def set_dz_url(metadata):
  function get_deezer_track (line 1281) | def get_deezer_track(url):
  function get_deezer_album (line 1289) | def get_deezer_album(url):
  function get_deezer_playlist (line 1301) | def get_deezer_playlist(url):
  function get_deezer_tracks (line 1314) | def get_deezer_tracks(url, login=True):
  function custom_art (line 1330) | def custom_art(text):
  function get_youtube_comments (line 1361) | def get_youtube_comments(url, limit=-1):  # -> generator
  function timestamp_to_time (line 1366) | def timestamp_to_time(text):
  function get_video_timestamps (line 1372) | def get_video_timestamps(video_info):
  function repeat_img_tooltip (line 1396) | def repeat_img_tooltip(repeat_setting):
  function create_progress_bar_texts (line 1404) | def create_progress_bar_texts(position, length):
  function truncate_title (line 1422) | def truncate_title(title):
  function drop_target_register (line 1430) | def drop_target_register(widget, *dndtypes):
  function dnd_bind (line 1434) | def dnd_bind(widget, sequence=None, func=None, add=None, need_cleanup=Tr...
  function get_cut_text (line 1450) | def get_cut_text(window, key):
  function start_on_login_win32 (line 1465) | def start_on_login_win32(working_dir, create_key=True, is_debug=True):
  function rm_old_startup_shortcuts (line 1481) | def rm_old_startup_shortcuts():
  function startfile (line 1491) | def startfile(file):
  function add_to_path (line 1503) | def add_to_path(path):
  function cmd_exists (line 1510) | def cmd_exists(cmd):
  function install_phantomjs (line 1518) | def install_phantomjs(install_directory):
Condensed preview — 98 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (937K chars).
[
  {
    "path": ".dockerignore",
    "chars": 627,
    "preview": ".ruff_cache/\n.pytest_cache/\ngit/\ngithub/\n.idea/\n.vscode/\n__pycache__/\nsrc/venv/\nsrc/.venv/\nbuild/\n_build/\ndist/\nimages/\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1981,
    "preview": "name: App Builder\non:\n  push:\n    branches: master\n  workflow_dispatch:\nconcurrency:\n  cancel-in-progress: true\n  # we w"
  },
  {
    "path": ".github/workflows/winget.yml",
    "chars": 308,
    "preview": "name: Publish to WinGet\non:\n  release:\n    types: [released]\njobs:\n  publish:\n    runs-on: windows-latest # action can o"
  },
  {
    "path": ".gitignore",
    "chars": 679,
    "preview": ".idea/\n.vscode/\n__pycache__/\nsrc/venv/\nsrc/.venv/\nbuild/\n_build/\ndist/\nimages/\ntest_files/\nvenv/\n.venv/\nsrc/.flatpak-bui"
  },
  {
    "path": "CHANGELOG.txt",
    "chars": 45909,
    "preview": "Music Caster Changelog\n\n5.25.2\n- [Fix] URL processing\n\n5.25.1\n- [Fix] Settings save/load ?\n- [Fix] Typing\n\n5.25.0\n- [Fix"
  },
  {
    "path": "Dockerfile",
    "chars": 1171,
    "preview": "# this images allows building music caster into a folder that can be run\n#\nFROM fedora:latest\nENV PY=python3.14\nENV PIP_"
  },
  {
    "path": "LICENSE",
    "chars": 1028,
    "preview": "Copyright © Elijah Lopez\n\nYou cannot sell Music Caster, unless there's an at least 50% royalty payment to the majority c"
  },
  {
    "path": "README.md",
    "chars": 7662,
    "preview": "\n<p align=\"center\"><img src=\"https://user-images.githubusercontent.com/21298211/171323258-5818355a-2c55-444b-8d0d-b0e3fe"
  },
  {
    "path": "build.cmd",
    "chars": 171,
    "preview": "@echo off\nset args=%1\nshift\n:start\nif [%1] == [] goto done\nset args=%args% %1\nshift\ngoto start\n:done\npython -m venv .ven"
  },
  {
    "path": "build.py",
    "chars": 27138,
    "preview": "#!/usr/bin/env python3\nimport argparse\nimport glob\nimport math\nimport os\nimport platform\nimport shutil\nimport sys\nimport"
  },
  {
    "path": "build_files/TkinterDnD2/TkinterDnD.py",
    "chars": 12642,
    "preview": "# -*- coding: utf-8 -*-\n\n\"\"\"Python wrapper for the tkdnd tk extension.\nThe tkdnd extension provides an interface to nati"
  },
  {
    "path": "build_files/TkinterDnD2/__init__.py",
    "chars": 442,
    "preview": "# dnd actions\nPRIVATE = 'private'\nNONE = 'none'\nASK = 'ask'\nCOPY = 'copy'\nMOVE = 'move'\nLINK = 'link'\nREFUSE_DROP = 'ref"
  },
  {
    "path": "build_files/Updater.cs.txt",
    "chars": 6964,
    "preview": "// NOTE: This was the old portable updater\n//  the new updater is updater.go\nusing System;\nusing System.Collections.Gen"
  },
  {
    "path": "build_files/Updater.exe.MANIFEST",
    "chars": 1678,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersi"
  },
  {
    "path": "build_files/daemon.spec",
    "chars": 2179,
    "preview": "# -*- mode: python ; coding: utf-8 -*-\nimport os\nfrom PyInstaller.building.api import PYZ, EXE\nfrom PyInstaller.building"
  },
  {
    "path": "build_files/flatpak-pip-generator.py",
    "chars": 15316,
    "preview": "#!/usr/bin/env python3\n\n__license__ = 'MIT'\n\nimport argparse\nimport json\nimport hashlib\nimport os\nimport shutil\nimport s"
  },
  {
    "path": "build_files/mc_version_info.txt",
    "chars": 1367,
    "preview": "# UTF-8\n# For more details about fixed file info 'ffi' see: http://msdn.microsoft.com/en-us/library/ms646997.aspx\nVSVers"
  },
  {
    "path": "build_files/mcu_version_info.txt",
    "chars": 881,
    "preview": "# UTF-8\nVSVersionInfo(\n    ffi=FixedFileInfo(\n        prodvers=(2, 3, 0, 0),\n        filevers=(2, 3, 0, 0),\n        mask"
  },
  {
    "path": "build_files/onedir.spec",
    "chars": 2328,
    "preview": "# -*- mode: python ; coding: utf-8 -*-\nimport os\nfrom PyInstaller.building.api import PYZ, EXE, COLLECT\nfrom PyInstaller"
  },
  {
    "path": "build_files/portable.spec",
    "chars": 2173,
    "preview": "# -*- mode: python ; coding: utf-8 -*-\nimport os\nfrom PyInstaller.building.api import PYZ, EXE\nfrom PyInstaller.building"
  },
  {
    "path": "build_files/setup_script.iss",
    "chars": 2990,
    "preview": "#define MyAppName \"Music Caster\"\n#define MyAppVersion \"5.25.1\"\n#define MyAppPublisher \"Elijah Lopez\"\n#define MyAppURL \"h"
  },
  {
    "path": "build_files/tkdnd2.9.2/pkgIndex.tcl",
    "chars": 257,
    "preview": "package ifneeded tkdnd 2.9.2 \\\n  \"source \\{$dir/tkdnd.tcl\\} ; \\\n   tkdnd::initialise \\{$dir\\} libtkdnd2.9.2[info sharedl"
  },
  {
    "path": "build_files/tkdnd2.9.2/tkdnd.tcl",
    "chars": 16121,
    "preview": "#\n# tkdnd.tcl --\n#\n#    This file implements some utility procedures that are used by the TkDND\n#    package.\n#\n# This s"
  },
  {
    "path": "build_files/tkdnd2.9.2/tkdnd_compat.tcl",
    "chars": 6231,
    "preview": "#\n# tkdnd_compat.tcl --\n# \n#    This file implements some utility procedures, to support older versions\n#    of the TkDN"
  },
  {
    "path": "build_files/tkdnd2.9.2/tkdnd_generic.tcl",
    "chars": 21065,
    "preview": "#\n# tkdnd_generic.tcl --\n#\n#    This file implements some utility procedures that are used by the TkDND\n#    package.\n#\n"
  },
  {
    "path": "build_files/tkdnd2.9.2/tkdnd_macosx.tcl",
    "chars": 6267,
    "preview": "#\n# tkdnd_macosx.tcl --\n#\n#    This file implements some utility procedures that are used by the TkDND\n#    package.\n\n# "
  },
  {
    "path": "build_files/tkdnd2.9.2/tkdnd_unix.tcl",
    "chars": 31443,
    "preview": "#\n# tkdnd_unix.tcl --\n#\n#    This file implements some utility procedures that are used by the TkDND\n#    package.\n#\n# T"
  },
  {
    "path": "build_files/tkdnd2.9.2/tkdnd_utils.tcl",
    "chars": 10148,
    "preview": "#\n# tkdnd_utils.tcl --\n#\n#    This file implements some utility procedures that are used by the TkDND\n#    package.\n#\n# "
  },
  {
    "path": "build_files/tkdnd2.9.2/tkdnd_windows.tcl",
    "chars": 7644,
    "preview": "#\n# tkdnd_windows.tcl --\n#\n#    This file implements some utility procedures that are used by the TkDND\n#    package.\n#\n"
  },
  {
    "path": "build_files/updater.spec",
    "chars": 1913,
    "preview": "# -*- mode: python ; coding: utf-8 -*-\nimport os\nimport sys\nfrom PyInstaller.building.api import PYZ, EXE\nfrom PyInstall"
  },
  {
    "path": "conftest.py",
    "chars": 802,
    "preview": "import pytest\n\n\ndef pytest_addoption(parser):\n    parser.addoption('--ci', action='store_true', default=False)\n    parse"
  },
  {
    "path": "linux_install.py",
    "chars": 167,
    "preview": "#!/usr/bin/env python3\nimport os\nfrom pathlib import Path\nINSTALL_DIR = Path('$HOME/bin/music-caster').expanduser()\n\nos."
  },
  {
    "path": "linux_install.sh",
    "chars": 1784,
    "preview": "#!/usr/bin/env bash\nset -ex\n\necho \"(music-caster) Updating...\"\n# git fetch\n# git reset --hard \"@{u}\"\n\nPYTHON=python3.14\n"
  },
  {
    "path": "music_caster.desktop",
    "chars": 931,
    "preview": "# See README.md for instructions on how to use\n[Desktop Entry]\nType=Application\nName=Music Caster\nComment=A modern music"
  },
  {
    "path": "pyproject.toml",
    "chars": 362,
    "preview": "[tool.ruff.format]\n# Like Black, use double quotes for strings.\nquote-style = \"single\"\n\n# pyproject.toml\n[tool.pytest.in"
  },
  {
    "path": "requirements-dev.txt",
    "chars": 360,
    "preview": "GitPython~=3.1\nautopep8\nrequirements-parser\n# pyinstaller dependencies\nsetuptools>=65.5.1\naltgraph\npyinstaller-hooks-con"
  },
  {
    "path": "requirements.txt",
    "chars": 1714,
    "preview": "audioop-lts; python_version>='3.13'\ncomtypes; sys_platform == 'win32'\nwheel\nbuild_files/pyaudio-0.2.14-cp314-cp314-win_a"
  },
  {
    "path": "resources/favicons/browserconfig.xml",
    "chars": 328,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo"
  },
  {
    "path": "resources/favicons/site.webmanifest",
    "chars": 590,
    "preview": "{\n    \"name\": \"\",\n    \"short_name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"https://raw.githubusercontent.com/e"
  },
  {
    "path": "resources/gude-2023-11-11.log",
    "chars": 1059,
    "preview": "19:31:21:330 [ALWAYS]\tGUDE Logging Started\n19:31:21:331 [INFO]\tSqliteResumeCache::SqliteResumeCache basedirectory is nul"
  },
  {
    "path": "scripts/arch-install.sh",
    "chars": 185,
    "preview": "#!/usr/bin/env bash\nPYTHON=$1\n\necho \"Installing Arch system dependencies\"\nsudo pacman -Sy --noconfirm python-pip $PYTHON"
  },
  {
    "path": "scripts/debian-install.sh",
    "chars": 480,
    "preview": "#!/usr/bin/env bash\necho \"debian-based distro detected\"\nPYTHON=$1\n# sudo apt install -y software-properties-common\n# Che"
  },
  {
    "path": "scripts/fedora-install.sh",
    "chars": 206,
    "preview": "#!/usr/bin/env bash\nPYTHON=$1\n\necho \"Installing Fedora system dependencies\"\nsudo dnf install -y \"$PYTHON\" python-pip \"$P"
  },
  {
    "path": "scripts/pre-req.sh",
    "chars": 1212,
    "preview": "#!/usr/bin/env bash\n# Define script locations (change paths if needed)\nDEBIAN_INSTALL=\"./scripts/debian-install.sh\"\nARCH"
  },
  {
    "path": "scripts/suse-install.sh",
    "chars": 173,
    "preview": "#!/usr/bin/env bash\nPYTHON=$1\n\necho \"Installing SUSE system dependencies\"\nsudo zypper install -y \"$PYTHON\" \"$PYTHON-deve"
  },
  {
    "path": "src/audio_player.py",
    "chars": 5294,
    "preview": "\"\"\"\nAudioPlayer v2.3.7\nAuthor: Elijah Lopez\nEnsure VLC shared library files (*.dll, *.so) are located in \"vlc_lib/\"\n\"\"\"\n"
  },
  {
    "path": "src/b64_images.py",
    "chars": 86988,
    "preview": "WINDOW_ICON = b'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAQAAAD2e2DtAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAA"
  },
  {
    "path": "src/ca.elijahlopez.MusicCaster.yml",
    "chars": 470,
    "preview": "app-id: ca.elijahlopez.MusicCaster\nruntime: org.freedesktop.Platform\nruntime-version: '21.08'\nsdk: org.freedesktop.Sdk\nf"
  },
  {
    "path": "src/experiments.py",
    "chars": 1716,
    "preview": "from base64 import b64decode\nfrom pathlib import Path\nimport urllib.parse\nimport soundfile as sf\nimport numpy as np\nimpo"
  },
  {
    "path": "src/go.mod",
    "chars": 24,
    "preview": "module Updater\n\ngo 1.17\n"
  },
  {
    "path": "src/go.sum",
    "chars": 165,
    "preview": "github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=\ngithub.com/akavel/rsrc v0.10.2/go.mod h1:"
  },
  {
    "path": "src/gui/__init__.py",
    "chars": 1391,
    "preview": "import FreeSimpleGUI as Sg\nimport platform\nfrom .views import *\nimport ctypes\nimport ctypes.wintypes\nimport sys\n\nALT_KEY"
  },
  {
    "path": "src/gui/components.py",
    "chars": 2844,
    "preview": "import base64\nimport io\nimport platform\n\nimport pyqrcode\nimport FreeSimpleGUI as Sg\nfrom meta import FONT_NORMAL, State\n"
  },
  {
    "path": "src/gui/views.py",
    "chars": 27748,
    "preview": "import platform\nimport time\nfrom datetime import datetime\nfrom math import ceil, floor\n\nimport FreeSimpleGUI as Sg\nfrom "
  },
  {
    "path": "src/knownpaths.py",
    "chars": 9999,
    "preview": "'''\nThe MIT License (MIT)\n\nCopyright (c) 2014 Michael Kropat\n\nPermission is hereby granted, free of charge, to any perso"
  },
  {
    "path": "src/languages/da.txt",
    "chars": 4218,
    "preview": "# Language: Danish\n# Credits: Frey Clante\n# Any line starting with a # is ignored\n# If a line contains $X do not transla"
  },
  {
    "path": "src/languages/de.txt",
    "chars": 5172,
    "preview": "# Sprache: Deutsch (German)\n# Credits: Bernd Miller\n# Jede Zeile, die mit einem # beginnt, wird ignoriert!\n# Wenn eine Z"
  },
  {
    "path": "src/languages/en.txt",
    "chars": 3821,
    "preview": "# Language: English\n# Credits: Elijah Lopez\n# Any line starting with a # is ignored\n# If a line contains $X do not trans"
  },
  {
    "path": "src/languages/es.txt",
    "chars": 5185,
    "preview": "# Language: Spanish\n# Credits: Sergi github.com/Varguit\n# Any line starting with a # is ignored!\n# If a line contains $X"
  },
  {
    "path": "src/languages/fr.txt",
    "chars": 5439,
    "preview": "# Language: French\n# Credits: github.com/tasye24\n# Any line starting with a # is ignored\n# If a line contains $X do not "
  },
  {
    "path": "src/languages/it.txt",
    "chars": 4791,
    "preview": "# Language: Italiano\n# Credits: Antonio Negrini github.com/antonio-negrini\n# Any line starting with a # is ignored!\n# If"
  },
  {
    "path": "src/languages/nl.txt",
    "chars": 4938,
    "preview": "# Language: Dutch\n# Credits: Jeffrey (PD3J) Jansen\n# Any line starting with a # is ignored!\n# If a line contains $X do n"
  },
  {
    "path": "src/languages/pt-br.txt",
    "chars": 4721,
    "preview": "# Idioma: Português - Brazil\n# Créditos: Cleiton Salvagni\n# Qualquer linha que começar com # é ignorada\n# Se uma linha c"
  },
  {
    "path": "src/languages/ru.txt",
    "chars": 5166,
    "preview": "# Language: Russian\n# Credits: Kostiantyn Astakhov\n# Any line starting with a # is ignored\n# If a line contains $X do no"
  },
  {
    "path": "src/languages/sk.txt",
    "chars": 4292,
    "preview": "# Language: Slovak\n# Credits: PalVac\n# Any line starting with a # is ignored\n# If a line contains $X do not translate th"
  },
  {
    "path": "src/languages/uk.txt",
    "chars": 5083,
    "preview": "# Language: Ukrainian\n# Credits: Kostiantyn Astakhov\n# Any line starting with a # is ignored\n# If a line contains $X do "
  },
  {
    "path": "src/meta.py",
    "chars": 2702,
    "preview": "VERSION = latest_version = '5.25.2'\r\nUPDATE_MESSAGE = \"\"\"\r\n[NEW] Support \"System Audio\" in CLI\r\n[MSG] Language translato"
  },
  {
    "path": "src/modules/db.py",
    "chars": 2896,
    "preview": "import sqlite3\nfrom pathlib import Path\nimport appdirs\nfrom meta import BUNDLE_IDENTIFIER\n\nuser_data_dir = Path(appdirs."
  },
  {
    "path": "src/modules/error_reporting.py",
    "chars": 116,
    "preview": "# TODO: move the following\n# app:report_album_art_buffer_error\n# app:handle_exception\n# utils:log_translation_error\n"
  },
  {
    "path": "src/modules/iph1papi.py",
    "chars": 8476,
    "preview": "# https://gist.github.com/NyaMisty/6c69c8f5681859b3b9ceb87737fabef7\nimport ctypes\nfrom ctypes import Structure, POINTER,"
  },
  {
    "path": "src/modules/playing_status.py",
    "chars": 1871,
    "preview": "import time\n\n\nclass PlayingStatus:\n    __slots__ = (\n        'NOT_PLAYING',\n        'PLAYING',\n        'PAUSED',\n       "
  },
  {
    "path": "src/modules/resolution_switcher.py",
    "chars": 6560,
    "preview": "import ctypes\nimport multiprocessing as mp\nimport platform\nfrom contextlib import suppress\nfrom functools import lru_cac"
  },
  {
    "path": "src/modules/url_metadata.py",
    "chars": 8300,
    "preview": "import time\nfrom pathlib import Path\nfrom typing import Self\nfrom audio_player import AudioPlayer\nimport requests\nimport"
  },
  {
    "path": "src/modules/win32_media_controls.py",
    "chars": 4247,
    "preview": "# https://learn.microsoft.com/windows/uwp/audio-video-camera/system-media-transport-controls\n# https://github.com/micros"
  },
  {
    "path": "src/music_caster.bat",
    "chars": 28,
    "preview": "pythonw \"music_caster.py\" -m"
  },
  {
    "path": "src/music_caster.py",
    "chars": 234427,
    "preview": "from gui.views import GuiContext\nfrom meta import (\n    State,\n    SUN_VALLEY_TCL,\n    PID_FILENAME,\n    LOCK_FILENAME,\n"
  },
  {
    "path": "src/pyoxidizer.bzl",
    "chars": 14126,
    "preview": "# This file defines how PyOxidizer application building and packaging is\n# performed. See PyOxidizer's documentation at\n"
  },
  {
    "path": "src/shared.py",
    "chars": 2183,
    "preview": "\"\"\"\nShared functions between app and build\n\"\"\"\nimport platform\nimport re\nfrom subprocess import DEVNULL, PIPE, Popen\n\n\nd"
  },
  {
    "path": "src/static/style.css",
    "chars": 10003,
    "preview": ":root {\n    --accent: #00bfff;\n}\n\nhtml {\n    font-size: large;\n}\n\nbody {\n    font-family: 'Roboto', Arial, Verdana, sans"
  },
  {
    "path": "src/sys_tray.py",
    "chars": 3717,
    "preview": "import multiprocessing as mp\nimport platform\nimport io\nimport sys\nfrom itertools import islice\nimport threading\nimport t"
  },
  {
    "path": "src/templates/index.html",
    "chars": 25536,
    "preview": "<!DOCTYPE html>\r\n<head>\r\n<title>Music Caster - {{device_name}}</title>\r\n<link rel=\"shortcut icon\" href=\"https://raw.gith"
  },
  {
    "path": "src/test_cases/ipconfig.py",
    "chars": 8203,
    "preview": "IPCONFIG_ELIBROFTW = '''Windows IP Configuration\n\n\nEthernet adapter Ethernet 3:\n\n   Media State . . . . . . . . . . . : "
  },
  {
    "path": "src/test_harness.py",
    "chars": 19467,
    "preview": "from base64 import b64decode\r\nfrom contextlib import suppress\r\nimport io\r\nfrom itertools import chain\r\nimport os\r\nimport"
  },
  {
    "path": "src/theme/LICENSE",
    "chars": 1064,
    "preview": "MIT License\n\nCopyright (c) 2021 rdbende\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
  },
  {
    "path": "src/theme/dark.tcl",
    "chars": 18013,
    "preview": "# Copyright © 2021 rdbende <rdbende@gmail.com>\n\n# A stunning dark theme for ttk based on Microsoft's Sun Valley visual s"
  },
  {
    "path": "src/theme/light.tcl",
    "chars": 18191,
    "preview": "# Copyright © 2021 rdbende <rdbende@gmail.com>\n\n# A stunning light theme for ttk based on Microsoft's Sun Valley visual "
  },
  {
    "path": "src/theme/sun-valley.tcl",
    "chars": 3148,
    "preview": "# Copyright © 2021 rdbende <rdbende@gmail.com>\n\nsource [file join [file dirname [info script]] light.tcl]\nsource [file j"
  },
  {
    "path": "src/updater.go",
    "chars": 4189,
    "preview": "package main\n\nimport (\n\t\"archive/zip\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n"
  },
  {
    "path": "src/utils.py",
    "chars": 61801,
    "preview": "# flake8: noqa: E402\nimport audioop\nimport base64\nimport ctypes\nimport glob\nimport io\nimport locale\nimport logging\nimpor"
  },
  {
    "path": "src/webview_demo.py",
    "chars": 501,
    "preview": "import webview\nimport platform\nimport sys\nimport os\nimport socket\n\nframeless = platform.system() == 'Windows'\n\ntry:\n    "
  }
]

// ... and 8 more files (download for full content)

About this extraction

This page contains the full source code of the elibroftw/music-caster GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 98 files (875.3 KB), approximately 255.2k tokens, and a symbol index with 455 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!