[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"cargo\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  workflow_dispatch:\n  push:\n    tags:\n      - \"*\"\n\nenv:\n  CARGO_TERM_COLOR: always\n  RUSTFLAGS: \"-C strip=symbols\"\n\njobs:\n  build:\n    name: Build for ${{ matrix.os}}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        include:\n          - os: ubuntu-latest\n            artifact_name: mpv-handler\n            asset_name: linux-amd64\n            target: x86_64-unknown-linux-musl\n\n          - os: windows-latest\n            artifact_name: mpv-handler.exe\n            artifact_name_debug: mpv-handler-debug.exe\n            asset_name: windows-amd64\n            asset_name_debug: windows-amd64-debug\n            target: x86_64-pc-windows-msvc\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Install dependencies\n        if: matrix.target == 'x86_64-unknown-linux-musl'\n        run: |\n          sudo apt-get install -y musl\n          rustup target add x86_64-unknown-linux-musl\n\n      - name: Build mpv-handler\n        run: |\n          cargo build --release --locked --target ${{ matrix.target }}\n\n      - name: Upload to artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: ${{ matrix.asset_name }}\n          path: target/${{ matrix.target }}/release/${{ matrix.artifact_name }}\n\n      - name: Build mpv-handler-debug for Windows\n        if: matrix.os == 'windows-latest'\n        run: |\n          cargo build --release --locked --target ${{ matrix.target }} `\n            --features console\n          move target/${{ matrix.target }}/release/${{ matrix.artifact_name }} `\n            target/${{ matrix.target }}/release/${{ matrix.artifact_name_debug }}\n\n      - name: Upload mpv-handler-debug to artifact\n        if: matrix.os == 'windows-latest'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ${{ matrix.asset_name_debug }}\n          path: target/${{ matrix.target }}/release/${{ matrix.artifact_name_debug }}\n\n  publish:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Download artifacts\n        uses: actions/download-artifact@v8\n\n      - name: Package Linux\n        run: |\n          zip -j mpv-handler-linux-amd64.zip \\\n            README*.md \\\n            share/linux/* \\\n            linux-amd64/mpv-handler\n\n      - name: Package Windows\n        run: |\n          zip -j mpv-handler-windows-amd64.zip \\\n            README*.md \\\n            share/windows/* \\\n            windows-amd64/mpv-handler.exe \\\n            windows-amd64-debug/mpv-handler-debug.exe\n\n      - name: SHA512SUM\n        run: |\n          sha512sum mpv-handler-*.zip > sha512sum\n\n      - name: Publish\n        uses: ncipollo/release-action@v1\n        with:\n          artifacts: \"mpv-handler-*.zip,sha512sum\"\n          generateReleaseNotes: true\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"mpv-handler\"\nversion = \"0.4.2\"\nedition = \"2024\"\nauthors = [\"Akatsuki Rui <akiirui@outlook.com>\"]\ndescription = \"Play website videos and songs with mpv & yt-dlp\"\nlicense = \"MIT\"\nreadme = \"README.md\"\nhomepage = \"https://github.com/akiirui/mpv-handler\"\nrepository = \"https://github.com/akiirui/mpv-handler\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nbase64 = \"0.22\"\ndirs = \"6.0\"\nserde = { version = \"1.0\", features = [\"derive\"] }\nthiserror = \"2.0\"\ntoml = \"1.1\"\n\n[features]\nconsole = []\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Akatsuki Rui\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[English][readme-en] | [简体中文][readme-zh-hans] | [繁体中文][readme-zh-hant]\n\n[readme-en]: https://github.com/akiirui/mpv-handler/blob/main/README.md\n[readme-zh-hans]: https://github.com/akiirui/mpv-handler/blob/main/README.zh-Hans.md\n[readme-zh-hant]: https://github.com/akiirui/mpv-handler/blob/main/README.zh-Hant.md\n\n# mpv handler\n\nA protocol handler for **mpv**, written by Rust.\n\nUse **mpv** and **yt-dlp** to play video and music from the websites.\n\nPlease use it with userscript:\n\n[![play-with-mpv][badges-play-with-mpv]][greasyfork-play-with-mpv]\n\n## Breaking changes\n\n### [v0.4.0][v0.4.0]\n\nTo avoid conflicts with the `mpv://` protocol provided by mpv.\n\n> mpv://...\n>\n> mpv protocol. This is used for starting mpv from URL handler. The protocol is stripped and the rest is passed to the player as a normal open argument. Only safe network protocols are allowed to be opened this way.\n\nScheme `mpv://` and `mpv-debug://` are deprecated, use `mpv-handler://` and `mpv-handler-debug://`.\n\n**Require manual intervention**\n\n#### Windows\n\nRun `handler-uninstall.bat` to uninstall deprecated protocol, and run `handler-install.bat` to install new procotol.\n\n#### Linux\n\nIf you installed manually, please repeat the manual installation process.\n\n## Protocol\n\n![](share/proto.png)\n\n### Scheme\n\n- `mpv-handler`: Run mpv-handler without console window\n- `mpv-handler-debug`: Run mpv-handler with console window to view outputs and errors\n\n### Plugins\n\n- `play`: Use mpv player to play video\n\n### Encoded Data\n\nUse [URL-safe base64][rfc-base64-url] to encode the URL or TITLE.\n\nReplace `/` to `_`, `+` to `-` and remove padding `=`.\n\nExample (JavaScript):\n\n```javascript\nlet data = btoa(\"https://www.youtube.com/watch?v=Ggkn2f5e-IU\");\nlet safe = data.replace(/\\//g, \"_\").replace(/\\+/g, \"-\").replace(/\\=/g, \"\");\n```\n\n### Parameters (Optional)\n\n```\ncookies  = [ www.domain.com.txt ]\nprofile  = [ default, low-latency, etc... ]\nquality  = [ 2160p, 1440p, 1080p, 720p, 480p, 360p ]\nv_codec  = [ av01, vp9, h265, h264 ]\nv_title  = [ Encoded Title ]\nsubfile  = [ Encoded URL ]\nstartat  = [ Seconds (float) ]\nreferrer = [ Encoded URL ]\n```\n\n## Installation\n\n### Linux\n\n#### Arch Linux\n\n[![mpv-handler][badges-aur]][download-aur]\n[![mpv-handler-git][badges-aur-git]][download-aur-git]\n\n#### Manual installation\n\n1. Download [latest Linux release][download-linux]\n2. Unzip the archive\n3. Copy `mpv-handler` to `$HOME/.local/bin`\n4. Copy `mpv-handler.desktop` to `$HOME/.local/share/applications/`\n5. Copy `mpv-handler-debug.desktop` to `$HOME/.local/share/applications/`\n6. Set executable permission for binary\n\n   - ```\n     $ chmod +x $HOME/.local/bin/mpv-handler\n     ```\n\n7. Register xdg-mime (thanks for the [linuxuprising][linuxuprising] reminder)\n\n   - ```\n     $ xdg-mime default mpv-handler.desktop x-scheme-handler/mpv-handler\n     $ xdg-mime default mpv-handler-debug.desktop x-scheme-handler/mpv-handler-debug\n     ```\n\n8. Add `$HOME/.local/bin` to your environment variable `PATH`\n9. **Optional**: _Copy `config.toml` to `$HOME/.config/mpv-handler/config.toml` and configure_\n\n### Windows\n\nWindows users need to install manually.\n\n#### Manual installation\n\n1. Download [latest Windows release][download-windows]\n2. Unzip the archive to the directory you want\n3. Run `handler-install.bat` to register protocol handler\n4. Edit `config.toml` and set `mpv` and `ytdl` path\n\n## Configuration\n\n```toml\nmpv = \"/usr/bin/mpv\"\n# Optional, Type: String\n# The path of mpv executable binary\n# Default value:\n# - Linux: mpv\n# - Windows: mpv.com\n\nytdl = \"/usr/bin/yt-dlp\"\n# Optional, Type: String\n# The path of yt-dlp executable binary\n\nproxy = \"http://example.com:8080\"\n# Optional, Type: String\n# HTTP(S) proxy server address\n\n# For Windows users:\n#   - The path can be \"C:\\\\folder\\\\some.exe\" or \"C:/folder/some.exe\"\n#   - The path target is an executable binary file, not a directory\n```\n\n[v0.4.0]: https://github.com/akiirui/mpv-handler/releases/tag/v0.4.0\n[rfc-base64-url]: https://datatracker.ietf.org/doc/html/rfc4648#section-5\n[badges-aur-git]: https://img.shields.io/aur/version/mpv-handler-git?style=for-the-badge&logo=archlinux&label=mpv-handler-git\n[badges-aur]: https://img.shields.io/aur/version/mpv-handler?style=for-the-badge&logo=archlinux&label=mpv-handler\n[badges-play-with-mpv]: https://img.shields.io/greasyfork/v/416271?style=for-the-badge&logo=greasyfork&label=play-with-mpv\n[download-aur-git]: https://aur.archlinux.org/packages/mpv-handler-git/\n[download-aur]: https://aur.archlinux.org/packages/mpv-handler/\n[download-linux]: https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-linux-amd64.zip\n[download-macos]: https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-macos-amd64.zip\n[download-windows]: https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-windows-amd64.zip\n[greasyfork-play-with-mpv]: https://greasyfork.org/scripts/416271-play-with-mpv\n[linuxuprising]: https://www.linuxuprising.com/2021/07/open-youtube-and-more-videos-from-your.html\n"
  },
  {
    "path": "README.zh-Hans.md",
    "content": "[English][readme-en] | [简体中文][readme-zh-hans] | [繁体中文][readme-zh-hant]\n\n[readme-en]: https://github.com/akiirui/mpv-handler/blob/main/README.md\n[readme-zh-hans]: https://github.com/akiirui/mpv-handler/blob/main/README.zh-Hans.md\n[readme-zh-hant]: https://github.com/akiirui/mpv-handler/blob/main/README.zh-Hant.md\n\n# mpv handler\n\n一个 **mpv** 的协议处理程序，使用 Rust 编写。\n\n使用 **mpv** 和 **yt-dlp** 播放网站上的视频与音乐。\n\n请配合用户脚本使用：\n\n[![play-with-mpv][badges-play-with-mpv]][greasyfork-play-with-mpv]\n\n## 重大变更\n\n### [v0.4.0][v0.4.0]\n\n为了避免与 mpv 所提供的 `mpv://` 协议冲突。\n\n> mpv://...\n>\n> mpv protocol. This is used for starting mpv from URL handler. The protocol is stripped and the rest is passed to the player as a normal open argument. Only safe network protocols are allowed to be opened this way.\n\n协议 `mpv://` 和 `mpv-debug://` 已弃用, 请使用 `mpv-handler://` 和 `mpv-handler-debug://`.\n\n**需要手动干预**\n\n#### Windows\n\n运行 `handler-uninstall.bat` 卸载已弃用的协议, 然后运行 `handler-install.bat` 安装新的协议.\n\n#### Linux\n\n如果你是手动安装的，请重新执行一遍手动安装流程。\n\n## 协议\n\n![](share/proto.png)\n\n### 协议名\n\n- `mpv-handler`: 在没有命令行窗口的情况下运行 mpv-handler\n- `mpv-handler-debug`: 在有命令行窗口的情况下运行 mpv-handler 以便于查看输出和错误\n\n### 插件 / Plugins\n\n- `play`: 使用 mpv 播放视频\n\n### 编码数据 / Encoded Data\n\n使用 [URL 安全的 base64][rfc-base64-url] 编码网址或标题。\n\n替换 `/` 至 `_`, `+` 至 `-` 并且删除填充的 `=`。\n\n示例 (JavaScript):\n\n```javascript\nlet data = btoa(\"https://www.youtube.com/watch?v=Ggkn2f5e-IU\");\nlet safe = data.replace(/\\//g, \"_\").replace(/\\+/g, \"-\").replace(/\\=/g, \"\");\n```\n\n### 参数 / Parameters (可选)\n\n```\ncookies  = [ www.domain.com.txt ]\nprofile  = [ default, low-latency, etc... ]\nquality  = [ 2160p, 1440p, 1080p, 720p, 480p, 360p ]\nv_codec  = [ av01, vp9, h265, h264 ]\nv_title  = [ Encoded Title ]\nsubfile  = [ Encoded URL ]\nstartat  = [ Seconds (float) ]\nreferrer = [ Encoded URL ]\n```\n\n## 安装\n\n### Linux\n\n#### Arch Linux\n\n[![mpv-handler][badges-aur]][download-aur]\n[![mpv-handler-git][badges-aur-git]][download-aur-git]\n\n#### 手动安装\n\n1. 下载 [最新的 Linux 压缩包][download-linux]\n2. 解压缩压缩包\n3. 复制 `mpv-handler` 至 `$HOME/.local/bin`\n4. 复制 `mpv-handler.desktop` 至 `$HOME/.local/share/applications/`\n5. 复制 `mpv-handler-debug.desktop` 至 `$HOME/.local/share/applications/`\n6. 为二进制文件设置可执行权限\n\n   - ```\n     $ chmod +x $HOME/.local/bin/mpv-handler\n     ```\n\n7. 注册 xdg-mime（感谢 [linuxuprising][linuxuprising] 的提醒）\n\n   - ```\n     $ xdg-mime default mpv-handler.desktop x-scheme-handler/mpv-handler\n     $ xdg-mime default mpv-handler-debug.desktop x-scheme-handler/mpv-handler-debug\n     ```\n\n8. 添加 `$HOME/.local/bin` 到环境变量 `PATH`\n9. **可选**: _复制 `config.toml` 至 `$HOME/.config/mpv-handler/config.toml` 并配置_\n\n### Windows\n\nWindows 用户目前只能手动安装。\n\n#### 手动安装\n\n1. 下载 [最新的 Windows 压缩包][download-windows]\n2. 解压缩档案到你想要的位置\n3. 运行 `handler-install.bat` 注册协议处理程序\n4. 编辑 `config.toml` 设置 `mpv` 和 `ytdl` 的路径\n\n## 配置\n\n```toml\nmpv = \"/usr/bin/mpv\"\n# 可选，类型：字符串\n# mpv 可执行文件的路径\n# 默认值:\n# - Linux: mpv\n# - Windows: mpv.com\n\nytdl = \"/usr/bin/yt-dlp\"\n# 可选，类型：字符串\n# yt-dlp 可执行文件的路径\n\nproxy = \"http://example.com:8080\"\n# 可选，类型：字符串\n# HTTP(S) 代理服务器的地址\n\n# 对于 Windows 用户：\n#   - 路径格式可以是 \"C:\\\\folder\\\\some.exe\"，也可以是 \"C:/folder/some.exe\"\n#   - 路径的目标是可执行二进制文件，而不是目录\n```\n\n[v0.4.0]: https://github.com/akiirui/mpv-handler/releases/tag/v0.4.0\n[rfc-base64-url]: https://datatracker.ietf.org/doc/html/rfc4648#section-5\n[badges-aur-git]: https://img.shields.io/aur/version/mpv-handler-git?style=for-the-badge&logo=archlinux&label=mpv-handler-git\n[badges-aur]: https://img.shields.io/aur/version/mpv-handler?style=for-the-badge&logo=archlinux&label=mpv-handler\n[badges-play-with-mpv]: https://img.shields.io/greasyfork/v/416271?style=for-the-badge&logo=greasyfork&label=play-with-mpv\n[download-aur-git]: https://aur.archlinux.org/packages/mpv-handler-git/\n[download-aur]: https://aur.archlinux.org/packages/mpv-handler/\n[download-linux]: https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-linux-amd64.zip\n[download-macos]: https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-macos-amd64.zip\n[download-windows]: https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-windows-amd64.zip\n[greasyfork-play-with-mpv]: https://greasyfork.org/scripts/416271-play-with-mpv\n[linuxuprising]: https://www.linuxuprising.com/2021/07/open-youtube-and-more-videos-from-your.html\n"
  },
  {
    "path": "README.zh-Hant.md",
    "content": "[English][readme-en] | [簡體中文][readme-zh-hans] | [繁體中文][readme-zh-hant]\n\n[readme-en]: https://github.com/akiirui/mpv-handler/blob/main/README.md\n[readme-zh-hans]: https://github.com/akiirui/mpv-handler/blob/main/README.zh-Hans.md\n[readme-zh-hant]: https://github.com/akiirui/mpv-handler/blob/main/README.zh-Hant.md\n\n# mpv handler\n\n一個 **mpv** 的協議處理程序，使用 Rust 編寫。\n\n使用 **mpv** 和 **yt-dlp** 播放網站上的視頻與音樂。\n\n請配合用戶腳本使用：\n\n[![play-with-mpv][badges-play-with-mpv]][greasyfork-play-with-mpv]\n\n## 重大變更\n\n### [v0.4.0][v0.4.0]\n\n爲了避免與 mpv 所提供的 `mpv://` 協議衝突。\n\n> mpv://...\n>\n> mpv protocol. This is used for starting mpv from URL handler. The protocol is stripped and the rest is passed to the player as a normal open argument. Only safe network protocols are allowed to be opened this way.\n\n協議 `mpv://` 和 `mpv-debug://` 已棄用, 請使用 `mpv-handler://` 和 `mpv-handler-debug://`.\n\n**需要手動干預**\n\n#### Windows\n\n運行 `handler-uninstall.bat` 卸載已棄用的協議, 然後運行 `handler-install.bat` 安裝新的協議.\n\n#### Linux\n\n如果你是手動安裝的，請重新執行一遍手動安裝流程。\n\n## 協議\n\n![](share/proto.png)\n\n### 協議名\n\n- `mpv-handler`: 在沒有命令行窗口的情況下運行 mpv-handler\n- `mpv-handler-debug`: 在有命令行窗口的情況下運行 mpv-handler 以便於查看輸出和錯誤\n\n### 插件 / Plugins\n\n- `play`: 使用 mpv 播放視頻\n\n### 編碼數據 / Encoded Data\n\n使用 [URL 安全的 base64][rfc-base64-url] 編碼網址或標題。\n\n替換 `/` 至 `_`, `+` 至 `-` 並且刪除填充的 `=`。\n\n示例 (JavaScript):\n\n```javascript\nlet data = btoa(\"https://www.youtube.com/watch?v=Ggkn2f5e-IU\");\nlet safe = data.replace(/\\//g, \"_\").replace(/\\+/g, \"-\").replace(/\\=/g, \"\");\n```\n\n### 參數 / Parameters (可選)\n\n```\ncookies  = [ www.domain.com.txt ]\nprofile  = [ default, low-latency, etc... ]\nquality  = [ 2160p, 1440p, 1080p, 720p, 480p, 360p ]\nv_codec  = [ av01, vp9, h265, h264 ]\nv_title  = [ Encoded Title ]\nsubfile  = [ Encoded URL ]\nstartat  = [ Seconds (float) ]\nreferrer = [ Encoded URL ]\n```\n\n## 安裝\n\n### Linux\n\n#### Arch Linux\n\n[![mpv-handler][badges-aur]][download-aur]\n[![mpv-handler-git][badges-aur-git]][download-aur-git]\n\n#### 手動安裝\n\n1. 下載 [最新的 Linux 壓縮包][download-linux]\n2. 解壓縮壓縮包\n3. 複製 `mpv-handler` 至 `$HOME/.local/bin`\n4. 複製 `mpv-handler.desktop` 至 `$HOME/.local/share/applications/`\n5. 複製 `mpv-handler-debug.desktop` 至 `$HOME/.local/share/applications/`\n6. 爲二進制文件設置可執行權限\n\n   - ```\n     $ chmod +x $HOME/.local/bin/mpv-handler\n     ```\n\n7. 註冊 xdg-mime（感謝 [linuxuprising][linuxuprising] 的提醒）\n\n   - ```\n     $ xdg-mime default mpv-handler.desktop x-scheme-handler/mpv-handler\n     $ xdg-mime default mpv-handler-debug.desktop x-scheme-handler/mpv-handler-debug\n     ```\n\n8. 添加 `$HOME/.local/bin` 到環境變量 `PATH`\n9. **可選**: _複製 `config.toml` 至 `$HOME/.config/mpv-handler/config.toml` 並配置_\n\n### Windows\n\nWindows 用戶目前只能手動安裝。\n\n#### 手動安裝\n\n1. 下載 [最新的 Windows 壓縮包][download-windows]\n2. 解壓縮檔案到你想要的位置\n3. 運行 `handler-install.bat` 註冊協議處理程序\n4. 編輯 `config.toml` 設置 `mpv` 和 `ytdl` 的路徑\n\n## 配置\n\n```toml\nmpv = \"/usr/bin/mpv\"\n# 可選，類型：字符串\n# mpv 可執行文件的路徑\n# 默認值:\n# - Linux: mpv\n# - Windows: mpv.com\n\nytdl = \"/usr/bin/yt-dlp\"\n# 可選，類型：字符串\n# yt-dlp 可執行文件的路徑\n\nproxy = \"http://example.com:8080\"\n# 可選，類型：字符串\n# HTTP(S) 代理服務器的地址\n\n# 對於 Windows 用戶：\n#   - 路徑格式可以是 \"C:\\\\folder\\\\some.exe\"，也可以是 \"C:/folder/some.exe\"\n#   - 路徑的目標是可執行二進制文件，而不是目錄\n```\n\n[v0.4.0]: https://github.com/akiirui/mpv-handler/releases/tag/v0.4.0\n[rfc-base64-url]: https://datatracker.ietf.org/doc/html/rfc4648#section-5\n[badges-aur-git]: https://img.shields.io/aur/version/mpv-handler-git?style=for-the-badge&logo=archlinux&label=mpv-handler-git\n[badges-aur]: https://img.shields.io/aur/version/mpv-handler?style=for-the-badge&logo=archlinux&label=mpv-handler\n[badges-play-with-mpv]: https://img.shields.io/greasyfork/v/416271?style=for-the-badge&logo=greasyfork&label=play-with-mpv\n[download-aur-git]: https://aur.archlinux.org/packages/mpv-handler-git/\n[download-aur]: https://aur.archlinux.org/packages/mpv-handler/\n[download-linux]: https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-linux-amd64.zip\n[download-macos]: https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-macos-amd64.zip\n[download-windows]: https://github.com/akiirui/mpv-handler/releases/latest/download/mpv-handler-windows-amd64.zip\n[greasyfork-play-with-mpv]: https://greasyfork.org/scripts/416271-play-with-mpv\n[linuxuprising]: https://www.linuxuprising.com/2021/07/open-youtube-and-more-videos-from-your.html\n"
  },
  {
    "path": "share/linux/config.toml",
    "content": "#mpv = \"/path/of/mpv\"\n# Optional, Type: String\n# The path of mpv executable binary\n# Default value:\n# - Linux: mpv\n# - Windows: mpv.com\n\n#ytdl = \"/path/of/ytdl\"\n# Optional, Type: String\n# The path of yt-dlp executable binary\n\n#proxy = \"http://example.com:8080\"\n# Optional, Type: String\n# HTTP(S) proxy server address\n"
  },
  {
    "path": "share/linux/mpv-handler-debug.desktop",
    "content": "[Desktop Entry]\nType=Application\nName=mpv-handler-debug\nGenericName=Multimedia player\nComment=Play website videos and songs with mpv & yt-dlp (Debug)\nNoDisplay=true\nIcon=mpv\nExec=mpv-handler %u\nTerminal=true\nCategories=AudioVideo;Audio;Video;Player;TV;\nMimeType=x-scheme-handler/mpv-handler-debug;\nX-KDE-Protocols=mpv-handler-debug\n"
  },
  {
    "path": "share/linux/mpv-handler.desktop",
    "content": "[Desktop Entry]\nType=Application\nName=mpv-handler\nGenericName=Multimedia player\nComment=Play website videos and songs with mpv & yt-dlp\nNoDisplay=true\nIcon=mpv\nExec=mpv-handler %u\nTerminal=false\nCategories=AudioVideo;Audio;Video;Player;TV;\nMimeType=x-scheme-handler/mpv-handler;\nX-KDE-Protocols=mpv-handler\n"
  },
  {
    "path": "share/proto.html",
    "content": "<html>\n  <head>\n    <style>\n      body {\n        height: fit-content;\n        width: max-content;\n        padding: 5px 5px 10px 5px;\n      }\n      p {\n        font-family: \"Roboto Condensed\";\n        font-size: 24px;\n      }\n      span {\n        border-style: solid;\n        border-width: 1px;\n        border-radius: 5px;\n        padding: 2px;\n        position: relative;\n      }\n      span::before {\n        content: \"\";\n        border-left: 5px solid transparent;\n        border-right: 5px solid transparent;\n        border-bottom: 5px solid black;\n        position: absolute;\n        bottom: -10px;\n        left: calc(50% - 5px);\n        transform: rotate(180deg);\n      }\n      span::after {\n        width: 200%;\n        font-size: 16px;\n        text-align: center;\n        position: absolute;\n        bottom: -30px;\n        left: -50%;\n      }\n\n      .scheme {\n        background: #cfff95;\n        border-color: #9ccc65;\n      }\n      .scheme::after {\n        content: \"Scheme\";\n      }\n\n      .plugins {\n        background: #73e8ff;\n        border-color: #29b6f6;\n      }\n      .plugins::after {\n        content: \"Plugins\";\n      }\n\n      .url {\n        background: #ffd95b;\n        border-color: #ffa726;\n      }\n      .url::after {\n        content: \"Encoded URL\";\n      }\n\n      .params {\n        background: #df78ef;\n        border-color: #ab47bc;\n      }\n      .params::after {\n        content: \"Parameters (Optional)\";\n      }\n    </style>\n  </head>\n  <body>\n    <p>\n      <span class=\"scheme\">mpv-handler</span>://<span class=\"plugins\">play</span\n      >/<span class=\"url\"\n        >aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj1HZ2tuMmY1ZS1JVQ</span\n      >/<span class=\"params\">?param1=value1&amp;param2=value2</span>\n    </p>\n  </body>\n</html>\n"
  },
  {
    "path": "share/windows/config.toml",
    "content": "#mpv = \"C:\\\\path\\\\of\\\\mpv.com\"\n# Optional, Type: String\n# The path of mpv executable binary\n# Default value:\n# - Linux: mpv\n# - Windows: mpv.com\n\n#ytdl = \"C:\\\\path\\\\of\\\\yt-dlp.exe\"\n# Optional, Type: String\n# The path of yt-dlp executable binary\n\n#proxy = \"http://example.com:8080\"\n# Optional, Type: String\n# HTTP(S) proxy server address\n\n# For Windows users:\n#   - The path can be \"C:\\\\folder\\\\some.exe\" or \"C:/folder/some.exe\"\n#   - The path is an executable binary file, not a directory\n"
  },
  {
    "path": "share/windows/handler-install.bat",
    "content": "@echo OFF\n\n:: Unattended install flag. When set, the script will not require user input.\nset unattended=no\nif \"%1\"==\"/u\" set unattended=yes\n\n:: Make sure this is Windows Vista or later\ncall :ensure_vista\n\n:: Make sure the script is running as admin\ncall :ensure_admin\n\n:: Get mpv.exe location\ncall :check_binary\n\n:: Add registry\ncall :add_verbs\n\n:die\n    if not [%1] == [] echo %~1\n    if [%unattended%] == [yes] exit 1\n    pause\n    exit 1\n\n:ensure_admin\n    :: 'openfiles' is just a commmand that is present on all supported Windows\n    :: versions, requires admin privileges and has no side effects, see:\n    :: https://stackoverflow.com/questions/4051883/batch-script-how-to-check-for-admin-rights\n    openfiles >nul 2>&1\n    if errorlevel 1 (\n        echo This batch script requires administrator privileges.\n        echo Right-click on handler-install.bat and select \"Run as administrator\".\n        call :die\n    )\n    goto :EOF\n\n:ensure_vista\n    ver | find \"XP\" >nul\n    if not errorlevel 1 (\n        echo This batch script only works on Windows Vista and later. To create file\n        echo associations on Windows XP, right click on a video file and use \"Open with...\".\n        call :die\n    )\n    goto :EOF\n\n:check_binary\n    cd /D %~dp0\n    set mpv_handler_conf=%cd%\\config.toml\n    set mpv_handler_path=%cd%\\mpv-handler.exe\n    set mpv_handler_debug_path=%cd%\\mpv-handler-debug.exe\n    if not exist \"%mpv_handler_conf%\" call :die \"Not found config.toml\"\n    if not exist \"%mpv_handler_path%\" call :die \"Not found mpv-handler.exe\"\n    if not exist \"%mpv_handler_debug_path%\" call :die \"Not found mpv-handler-debug.exe\"\n    goto :EOF\n\n:reg\n    :: Wrap the reg command to check for errors\n    >nul reg %*\n    if errorlevel 1 set error=yes\n    if [%error%] == [yes] echo Error in command: reg %*\n    if [%error%] == [yes] call :die\n    goto :EOF\n\n:add_verbs\n    :: Add the mpv protocol to the registry\n    call :reg add \"HKCR\\mpv-handler\" /d \"URL:MPV Handler\" /f\n    call :reg add \"HKCR\\mpv-handler\" /v \"Content Type\" /d \"application/x-mpv-handler\" /f\n    call :reg add \"HKCR\\mpv-handler\" /v \"URL Protocol\" /f\n    call :reg add \"HKCR\\mpv-handler\\DefaultIcon\" /d \"\\\"%mpv_exe_path%\\\",1\" /f\n    call :reg add \"HKCR\\mpv-handler\\shell\\open\\command\" /d \"\\\"%mpv_handler_path%\\\" \\\"%%%%1\\\"\" /f\n\n    :: Add the mpv protocol to the registry\n    call :reg add \"HKCR\\mpv-handler-debug\" /d \"URL:MPV Handler Debug\" /f\n    call :reg add \"HKCR\\mpv-handler-debug\" /v \"Content Type\" /d \"application/x-mpv-handler-debug\" /f\n    call :reg add \"HKCR\\mpv-handler-debug\" /v \"URL Protocol\" /f\n    call :reg add \"HKCR\\mpv-handler-debug\\DefaultIcon\" /d \"\\\"%mpv_exe_path%\\\",1\" /f\n    call :reg add \"HKCR\\mpv-handler-debug\\shell\\open\\command\" /d \"\\\"%mpv_handler_debug_path%\\\" \\\"%%%%1\\\"\" /f\n\n    echo Successfully installed mpv-handler\n    echo Enjoy!\n\n    goto :EOF\n"
  },
  {
    "path": "share/windows/handler-uninstall.bat",
    "content": "@echo OFF\n\n:: Unattended install flag. When set, the script will not require user input.\nset unattended=no\nif \"%1\"==\"/u\" set unattended=yes\n\n:: Make sure this is Windows Vista or later\ncall :ensure_vista\n\n:: Make sure the script is running as admin\ncall :ensure_admin\n\n:: Delete registry\ncall :del_verbs\n\n\n\n:die\n    if not [%1] == [] echo %~1\n    if [%unattended%] == [yes] exit 1\n    pause\n    exit 1\n\n:ensure_admin\n    :: 'openfiles' is just a commmand that is present on all supported Windows\n    :: versions, requires admin privileges and has no side effects, see:\n    :: https://stackoverflow.com/questions/4051883/batch-script-how-to-check-for-admin-rights\n    openfiles >nul 2>&1\n    if errorlevel 1 (\n        echo This batch script requires administrator privileges.\n        echo Right-click on handler-uninstall.bat and select \"Run as administrator\".\n        call :die\n    )\n    goto :EOF\n\n:ensure_vista\n    ver | find \"XP\" >nul\n    if not errorlevel 1 (\n        echo This batch script only works on Windows Vista and later. To create file\n        echo associations on Windows XP, right click on a video file and use \"Open with...\".\n        call :die\n    )\n    goto :EOF\n\n:reg\n    :: Wrap the reg command to check for errors\n    >nul reg %*\n    if errorlevel 1 set error=yes\n    if [%error%] == [yes] echo Error in command: reg %*\n    if [%error%] == [yes] call :die\n    goto :EOF\n\n:del_verbs\n    :: Delete deprecated mpv and mpv-debug protocol\n    call :reg delete \"HKCR\\mpv\" /f\n    call :reg delete \"HKCR\\mpv-debug\" /f\n\n    :: Delete protocol\n    call :reg delete \"HKCR\\mpv-handler\" /f\n    call :reg delete \"HKCR\\mpv-handler-debug\" /f\n\n    echo Successfully uninstalled mpv-handler\n\n    goto :EOF\n"
  },
  {
    "path": "src/config.rs",
    "content": "use crate::error::Error;\nuse serde::Deserialize;\nuse std::path::PathBuf;\n\n/// Config of mpv-handler\n///\n/// - `mpv`: mpv binary path\n/// - `ytdl`: yt-dlp binary path\n/// - `proxy: HTTP(S) proxy server address\n#[derive(Debug, Deserialize)]\npub struct Config {\n    pub mpv: Option<String>,\n    pub ytdl: Option<String>,\n    pub proxy: Option<String>,\n}\n\nimpl Config {\n    /// Load config file and retruns `Config`\n    ///\n    /// If config file doesn't exists, returns default value\n    pub fn load() -> Result<Config, Error> {\n        if let Some(mut path) = get_config_dir() {\n            path.push(\"config.toml\");\n\n            if path.exists() {\n                let data: String = std::fs::read_to_string(&path)?;\n                let mut config: Config = toml::from_str(&data)?;\n\n                if let Some(mpv) = config.mpv {\n                    config.mpv = Some(realpath(mpv)?);\n                }\n                if let Some(ytdl) = config.ytdl {\n                    config.ytdl = Some(realpath(ytdl)?);\n                }\n\n                return Ok(config);\n            }\n        }\n\n        Ok(default_config())\n    }\n}\n\n/// Returns config directory path of mpv-handler\npub fn get_config_dir() -> Option<PathBuf> {\n    // Linux config directory location: $XDG_CONFIG_HOME/mpv-handler/\n    #[cfg(unix)]\n    {\n        if let Some(mut v) = dirs::config_dir() {\n            v.push(\"mpv-handler\");\n            return Some(v);\n        }\n    }\n\n    // Windows config directory location: %WORKING_DIR%\\\n    #[cfg(windows)]\n    {\n        if let Ok(mut v) = std::env::current_exe() {\n            v.pop();\n            return Some(v);\n        }\n    }\n\n    eprintln!(\"Failed to get config directory\");\n    None\n}\n\n/// The default value of `Config.mpv`\npub fn default_mpv() -> Result<String, Error> {\n    #[cfg(unix)]\n    return realpath(\"mpv\");\n    #[cfg(windows)]\n    return realpath(\"mpv.com\");\n}\n\n/// The defalut value of `Config`\nfn default_config() -> Config {\n    Config {\n        mpv: None,\n        ytdl: None,\n        proxy: None,\n    }\n}\n\nfn realpath<T: AsRef<std::ffi::OsStr>>(path: T) -> Result<String, Error> {\n    let path = std::path::PathBuf::from(&path);\n\n    if path.is_relative() {\n        #[cfg(windows)]\n        {\n            if let Some(mut p) = crate::config::get_config_dir() {\n                p.push(&path);\n                if let Ok(rp) = p.canonicalize() {\n                    return Ok(rp.display().to_string());\n                };\n            }\n        }\n\n        if let Some(paths) = std::env::var_os(\"PATH\") {\n            for mut p in std::env::split_paths(&paths) {\n                p.push(&path);\n                if let Ok(rp) = p.canonicalize() {\n                    return Ok(rp.display().to_string());\n                };\n            }\n        }\n    }\n\n    Ok(path.display().to_string())\n}\n\n#[test]\nfn test_config_parse() {\n    // Custom values\n    let config: Config = toml::from_str(\n        r#\"\n            mpv = \"/usr/bin/mpv\"\n            ytdl = \"/usr/bin/yt-dlp\"\n            proxy = \"http://example.com:8080\"\n        \"#,\n    )\n    .unwrap();\n\n    assert_eq!(config.mpv, Some(\"/usr/bin/mpv\".to_string()));\n    assert_eq!(config.ytdl, Some(\"/usr/bin/yt-dlp\".to_string()));\n    assert_eq!(config.proxy, Some(\"http://example.com:8080\".to_string()));\n\n    // Unexpected values\n    let config: Config = toml::from_str(\n        r#\"\n            key1 = \"value1\"\n            key2 = \"value2\"\n            key3 = \"value3\"\n        \"#,\n    )\n    .unwrap();\n\n    #[cfg(unix)]\n    assert_eq!(config.mpv, None);\n    assert_eq!(config.ytdl, None);\n    assert_eq!(config.proxy, None);\n}\n"
  },
  {
    "path": "src/error.rs",
    "content": "use thiserror::Error;\n\n#[derive(Debug, Error)]\npub enum Error {\n    #[error(\"Too many arguments\")]\n    TooManyArgs,\n    #[error(\"Incorrect protocol \\\"{0}\\\"\")]\n    IncorrectProtocol(String),\n    #[error(\"Incorrect video URL \\\"{0}\\\"\")]\n    IncorrectVideoURL(String),\n    #[error(\"Dangerous video protocol \\\"{0}\\\"\")]\n    DangerousVideoProtocol(String),\n    #[error(\"Player exited by error\")]\n    PlayerExited(u8),\n    #[error(\"Failed to run player ({0})\")]\n    PlayerRunFailed(std::io::Error),\n    #[error(\"Failed to decode ({0})\")]\n    FromBase64Error(#[from] base64::DecodeError),\n    #[error(\"Failed to decode ({0})\")]\n    FromStringError(#[from] std::string::FromUtf8Error),\n    #[error(\"Failed to decode ({0})\")]\n    FromTomlError(#[from] toml::de::Error),\n    #[error(\"Failed to decode ({0})\")]\n    FromIoError(#[from] std::io::Error),\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#![cfg_attr(\n    all(target_os = \"windows\", not(feature = \"console\"), not(debug_assertions)),\n    windows_subsystem = \"windows\"\n)]\n\nmod config;\nmod error;\nmod plugins;\nmod protocol;\n\nuse std::process::ExitCode;\n\nuse crate::config::Config;\nuse crate::error::Error;\nuse crate::plugins::Plugins;\nuse crate::protocol::Protocol;\n\nfn main() -> ExitCode {\n    match run() {\n        Ok(_) => ExitCode::SUCCESS,\n        Err(e) => print_error(e),\n    }\n}\n\n/// Run handler\nfn run() -> Result<(), Error> {\n    let args: Vec<String> = std::env::args().collect();\n    let arg: &str = match args.len() {\n        2 => &args[1],\n        1 => return Ok(print_usage()),\n        _ => return Err(Error::TooManyArgs),\n    };\n\n    let proto = Protocol::parse(arg)?;\n    let config = Config::load()?;\n\n    // Call plugin by scheme\n    match proto.plugin {\n        Plugins::Play => crate::plugins::play::exec(&proto, &config),\n    }\n}\n\n/// Print usage\nfn print_usage() {\n    let version: &str = option_env!(\"MPV_HANDLER_VERSION\").unwrap_or(env!(\"CARGO_PKG_VERSION\"));\n\n    println!(\"mpv-handler {}\\n\", version);\n    println!(\"Usage:\\n  {}\\n\", \"mpv-handler <url>\",);\n}\n\n/// Print error\nfn print_error(e: Error) -> ExitCode {\n    eprint!(\"{e}\");\n    std::io::Read::read(&mut std::io::stdin(), &mut []).unwrap();\n\n    match e {\n        Error::PlayerExited(code) => ExitCode::from(code),\n        _ => ExitCode::FAILURE,\n    }\n}\n"
  },
  {
    "path": "src/plugins/mod.rs",
    "content": "pub mod play;\n\n#[derive(Debug, PartialEq)]\npub enum Plugins {\n    Play,\n}\n"
  },
  {
    "path": "src/plugins/play.rs",
    "content": "use crate::config::Config;\nuse crate::error::Error;\nuse crate::protocol::Protocol;\n\nconst PREFIX_COOKIES: &str = \"--ytdl-raw-options-append=cookies=\";\nconst PREFIX_PROFILE: &str = \"--profile=\";\nconst PREFIX_FORMATS: &str = \"--ytdl-raw-options-append=format-sort=\";\nconst PREFIX_V_TITLE: &str = \"--title=\";\nconst PREFIX_SUBFILE: &str = \"--sub-file=\";\nconst PREFIX_STARTAT: &str = \"--start=\";\nconst PREFIX_REFERRER: &str = \"--referrer=\";\nconst PREFIX_YT_PATH: &str = \"--script-opts=ytdl_hook-ytdl_path=\";\n\n/// Execute player with given options\npub fn exec(proto: &Protocol, config: &Config) -> Result<(), Error> {\n    let mut options: Vec<&str> = Vec::new();\n    let option_cookies: String;\n    let option_profile: String;\n    let option_formats: String;\n    let option_v_title: String;\n    let option_subfile: String;\n    let option_startat: String;\n    let option_yt_path: String;\n    let option_referrer: String;\n\n    // Append cookies option\n    if let Some(v) = proto.cookies {\n        if let Some(v) = cookies(v) {\n            option_cookies = v;\n            options.push(&option_cookies);\n        }\n    }\n\n    // Append profile option\n    if let Some(v) = proto.profile {\n        option_profile = profile(v);\n        options.push(&option_profile);\n    }\n\n    // Append formats option\n    if proto.quality.is_some() || proto.v_codec.is_some() {\n        if let Some(v) = formats(proto.quality, proto.v_codec) {\n            option_formats = v;\n            options.push(&option_formats);\n        }\n    }\n\n    // Append v_title option\n    if let Some(v) = &proto.v_title {\n        option_v_title = v_title(v);\n        options.push(&option_v_title);\n    }\n\n    // Append subfile option\n    if let Some(v) = &proto.subfile {\n        option_subfile = subfile(v);\n        options.push(&option_subfile);\n    }\n\n    // Append startat option\n    if let Some(v) = &proto.startat {\n        option_startat = startat(v);\n        options.push(&option_startat);\n    }\n\n    // Append referrer option\n    if let Some(v) = &proto.referrer {\n        option_referrer = referrer(v);\n        options.push(&option_referrer);\n    }\n\n    // Set custom ytdl execute file path\n    if let Some(v) = &config.ytdl {\n        option_yt_path = yt_path(v);\n        options.push(&option_yt_path);\n    }\n\n    // Print binaries and options list (in debug build)\n    if &proto.scheme == &crate::protocol::Schemes::MpvHandlerDebug || cfg!(debug_assertions) {\n        // Print binaries\n        println!(\"Binaries:\");\n\n        if let Some(v) = &config.mpv {\n            println!(\"    {}\", v);\n        } else {\n            println!(\"    {}\", crate::config::default_mpv()?);\n        }\n\n        if let Some(v) = &config.ytdl {\n            println!(\"    {}\", v);\n        }\n\n        // Print options list\n        if !options.is_empty() {\n            println!(\"Options:\");\n            for option in &options {\n                println!(\"    {}\", option);\n            }\n        }\n    }\n\n    // Print video URL\n    println!(\"Playing: {}\", proto.url);\n\n    // Execute mpv player\n    let mut command;\n\n    if let Some(v) = &config.mpv {\n        command = std::process::Command::new(v);\n    } else {\n        command = std::process::Command::new(crate::config::default_mpv()?);\n    }\n\n    command.args(&options).arg(\"--\").arg(&proto.url);\n\n    // Hide console window on Windows if not in debug mode\n    #[cfg(windows)]\n    {\n        use std::os::windows::process::CommandExt;\n        if &proto.scheme == &crate::protocol::Schemes::MpvHandler && !cfg!(debug_assertions) {\n            command.creation_flags(0x08000000);\n        }\n    }\n\n    // Set HTTP(S) proxy environment variables\n    if let Some(proxy) = &config.proxy {\n        command.env(\"http_proxy\", proxy);\n        command.env(\"HTTP_PROXY\", proxy);\n        command.env(\"https_proxy\", proxy);\n        command.env(\"HTTPS_PROXY\", proxy);\n    }\n\n    // Fix some browsers to overwrite \"LD_LIBRARY_PATH\" on Linux\n    // It will be broken mpv player\n    // mpv: symbol lookup error: mpv: undefined symbol: vkCreateWaylandSurfaceKHR\n    #[cfg(unix)]\n    command.env_remove(\"LD_LIBRARY_PATH\");\n\n    // Fix Vivaldi to overwrite \"LD_PRELOAD\" on Linux\n    // https://github.com/akiirui/mpv-handler/issues/78\n    #[cfg(unix)]\n    command.env_remove(\"LD_PRELOAD\");\n\n    match command.status() {\n        Ok(o) => match o.code() {\n            Some(code) => match code {\n                0 => Ok(()),\n                _ => Err(Error::PlayerExited(code as u8)),\n            },\n            None => Ok(()),\n        },\n        Err(e) => Err(Error::PlayerRunFailed(e)),\n    }\n}\n\n/// Return cookies option\nfn cookies(cookies: &str) -> Option<String> {\n    match crate::config::get_config_dir() {\n        Some(mut p) => {\n            p.push(\"cookies\");\n            p.push(cookies);\n\n            if p.exists() {\n                let cookies = p.display();\n                return Some(format!(\"{PREFIX_COOKIES}{cookies}\"));\n            } else {\n                eprintln!(\"Cookies file not found \\\"{}\\\"\", p.display());\n                return None;\n            }\n        }\n        None => None,\n    }\n}\n\n/// Return profile option\nfn profile(profile: &str) -> String {\n    format!(\"{PREFIX_PROFILE}{profile}\")\n}\n\n/// Return formats option\nfn formats(quality: Option<&str>, v_codec: Option<&str>) -> Option<String> {\n    let mut f: Vec<String> = Vec::new();\n    let formats: String;\n\n    if let Some(v) = quality {\n        let i: String = v.matches(char::is_numeric).collect();\n        f.push(format!(\"res:{i}\"));\n    }\n\n    if let Some(v) = v_codec {\n        f.push(format!(\"+vcodec:{v}\"))\n    }\n\n    formats = f.join(\",\");\n\n    Some(format!(\"{PREFIX_FORMATS}{formats}\"))\n}\n\n/// Return v_title option\nfn v_title(v_title: &str) -> String {\n    format!(\"{PREFIX_V_TITLE}{v_title}\")\n}\n\n/// Return subfile option\nfn subfile(subfile: &str) -> String {\n    format!(\"{PREFIX_SUBFILE}{subfile}\")\n}\n\n/// Return startat option\nfn startat(startat: &str) -> String {\n    format!(\"{PREFIX_STARTAT}{startat}\")\n}\n\n/// Return referrer option\nfn referrer(referrer: &str) -> String {\n    format!(\"{PREFIX_REFERRER}{referrer}\")\n}\n\n/// Return yt_path option\nfn yt_path(yt_path: &str) -> String {\n    format!(\"{PREFIX_YT_PATH}{yt_path}\")\n}\n\n#[test]\nfn test_profile_option() {\n    let p = profile(\"low-latency\");\n    assert_eq!(p, format!(\"{PREFIX_PROFILE}low-latency\"));\n}\n\n#[test]\nfn test_formats_option() {\n    // Only quality\n    let q = formats(Some(\"720p\"), None);\n    assert_eq!(q.unwrap(), format!(\"{PREFIX_FORMATS}res:720\"));\n\n    // Only v_codec\n    let v = formats(None, Some(\"vp9\"));\n    assert_eq!(v.unwrap(), format!(\"{PREFIX_FORMATS}+vcodec:vp9\"));\n\n    // Both quality and v_codec\n    let qv = formats(Some(\"720p\"), Some(\"vp9\"));\n    assert_eq!(qv.unwrap(), format!(\"{PREFIX_FORMATS}res:720,+vcodec:vp9\"));\n}\n#[test]\nfn test_v_title_option() {\n    let t = v_title(\"Hello World!\");\n    assert_eq!(t, format!(\"{PREFIX_V_TITLE}Hello World!\"));\n}\n\n#[test]\nfn test_subfile_option() {\n    let s = subfile(\"http://example.com/en.ass\");\n    assert_eq!(s, format!(\"{PREFIX_SUBFILE}http://example.com/en.ass\"));\n}\n\n#[test]\nfn test_startat_option() {\n    let s = startat(\"233\");\n    assert_eq!(s, format!(\"{PREFIX_STARTAT}233\"));\n}\n\n#[test]\nfn test_referrer_option() {\n    let r = referrer(\"http://example.com/\");\n    assert_eq!(r, format!(\"{PREFIX_REFERRER}http://example.com/\"));\n}\n\n#[test]\nfn test_yt_path_option() {\n    let y = yt_path(\"/usr/bin/yt-dlp\");\n    assert_eq!(y, format!(\"{PREFIX_YT_PATH}/usr/bin/yt-dlp\"));\n}\n"
  },
  {
    "path": "src/protocol.rs",
    "content": "use crate::error::Error;\nuse crate::plugins::Plugins;\n\n#[derive(Debug, PartialEq)]\npub enum Schemes {\n    MpvHandler,\n    MpvHandlerDebug,\n}\n\nconst SAFE_PROTOS: [&str; 11] = [\n    \"http\", \"https\", \"ftp\", \"ftps\", \"rtmp\", \"rtmps\", \"rtmpe\", \"rtmpt\", \"rtmpts\", \"rtmpte\", \"data\",\n];\n\n/// Protocol of mpv-handler\n///\n/// ```\n/// mpv-handler://PLUGINS/ENCODED_URL/?PARAMETERS=VALUES\n/// mpv-handler-debug://PLUGINS/ENCODED_URL/?PARAMETERS=VALUES\n/// ```\n///\n/// PLUGINS:\n/// - play\n///\n/// ENCODED_URL:\n/// - URL-safe base64 encoded URL\n///\n/// PARAMETERS:\n/// - cookies\n/// - profile\n/// - quality\n/// - v_codec\n/// - v_title\n/// - subfile\n/// - startat\n/// - referrer\n#[derive(Debug, PartialEq)]\npub struct Protocol<'a> {\n    pub scheme: Schemes,\n    pub plugin: Plugins,\n    pub url: String,\n    pub cookies: Option<&'a str>,\n    pub profile: Option<&'a str>,\n    pub quality: Option<&'a str>,\n    pub v_codec: Option<&'a str>,\n    pub v_title: Option<String>,\n    pub subfile: Option<String>,\n    pub startat: Option<&'a str>,\n    pub referrer: Option<String>,\n}\n\nimpl Protocol<'_> {\n    /// Parse the given argument and returns `Protocol`\n    pub fn parse(arg: &str) -> Result<Protocol<'_>, Error> {\n        let scheme;\n        let plugin;\n        let url;\n        let mut cookies: Option<&str> = None;\n        let mut profile: Option<&str> = None;\n        let mut quality: Option<&str> = None;\n        let mut v_codec: Option<&str> = None;\n        let mut v_title: Option<String> = None;\n        let mut subfile: Option<String> = None;\n        let mut startat: Option<&str> = None;\n        let mut referrer: Option<String> = None;\n\n        let mut i: usize;\n\n        // Check scheme `mpv-handler://` and `mpv-handler-debug://`\n        (i, scheme) = if let Some(s) = arg.find(\"://\") {\n            match &arg[..s] {\n                \"mpv-handler\" => (s + \"://\".len(), Schemes::MpvHandler),\n                \"mpv-handler-debug\" => (s + \"://\".len(), Schemes::MpvHandlerDebug),\n                _ => return Err(Error::IncorrectProtocol(arg.to_string())),\n            }\n        } else {\n            return Err(Error::IncorrectProtocol(arg.to_string()));\n        };\n\n        // Get plugin\n        (i, plugin) = if let Some(s) = arg[i..].find('/') {\n            match &arg[i..i + s] {\n                \"play\" => (i + s + 1, Plugins::Play),\n                _ => return Err(Error::IncorrectProtocol(arg.to_string())),\n            }\n        } else {\n            return Err(Error::IncorrectProtocol(arg.to_string()));\n        };\n\n        // Get url and decode by base64\n        (i, url) = if let Some(s) = arg[i..].find('/') {\n            (i + s + 1, decode_url(&arg[i..i + s])?)\n        } else {\n            (arg.len(), decode_url(&arg[i..])?)\n        };\n\n        // Get parameters\n        if let Some(s) = arg[i..].find('?') {\n            let params: Vec<&str> = arg[i + s + 1..].split('&').collect();\n\n            for param in params {\n                let data: Vec<&str> = param.split_terminator('=').collect();\n\n                if data.len() != 2 {\n                    return Err(Error::IncorrectProtocol(arg.to_string()));\n                }\n\n                let k = data[0];\n                let v = data[1];\n\n                match k {\n                    \"cookies\" => cookies = Some(v),\n                    \"profile\" => profile = Some(v),\n                    \"quality\" => quality = Some(v),\n                    \"v_codec\" => v_codec = Some(v),\n                    \"v_title\" => v_title = Some(decode_txt(v)?),\n                    \"subfile\" => subfile = Some(decode_url(v)?),\n                    \"startat\" => startat = Some(v),\n                    \"referrer\" => referrer = Some(decode_txt(v)?),\n                    _ => {}\n                };\n            }\n        }\n\n        Ok(Protocol {\n            scheme,\n            plugin,\n            url,\n            cookies,\n            profile,\n            quality,\n            v_codec,\n            v_title,\n            subfile,\n            startat,\n            referrer,\n        })\n    }\n}\n\n/// Decode base64 data (URL-safe) and return `String`\nfn decode_txt(data: &str) -> Result<String, Error> {\n    Ok(String::from_utf8(base64::Engine::decode(\n        &base64::prelude::BASE64_URL_SAFE_NO_PAD,\n        data,\n    )?)?)\n}\n\n/// Decode base64 data (URL-safe) and check URL protocol\n///\n/// Allowed protocols:\n///\n/// ```\n/// \"http\", \"https\", \"ftp\", \"ftps\", \"rtmp\", \"rtmps\",\n/// \"rtmpe\", \"rtmpt\", \"rtmpts\", \"rtmpte\", \"data\"\n/// ```\nfn decode_url(data: &str) -> Result<String, Error> {\n    let url = decode_txt(data)?;\n\n    match url.find(\"://\") {\n        Some(s) => {\n            if !SAFE_PROTOS.contains(&&url[..s]) {\n                return Err(Error::DangerousVideoProtocol(url[..s].to_string()));\n            }\n        }\n        None => return Err(Error::IncorrectVideoURL(url)),\n    };\n\n    Ok(url)\n}\n\n#[test]\nfn test_protocol_parse() {\n    // All parameters\n    let proto =\n        Protocol::parse(\"mpv-handler://play/aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj1HZ2tuMmY1ZS1JVQ/?cookies=www.youtube.com.txt&profile=low-latency&quality=1080p&v_codec=av01&v_title=VGl0bGU&subfile=aHR0cDovL2V4YW1wbGUuY29tL2VuLmFzcw&startat=233&referrer=aHR0cHM6Ly93d3cueW91dHViZS5jb20v\").unwrap();\n\n    assert_eq!(proto.scheme, Schemes::MpvHandler);\n    assert_eq!(proto.plugin, Plugins::Play);\n    assert_eq!(proto.url, \"https://www.youtube.com/watch?v=Ggkn2f5e-IU\");\n    assert_eq!(proto.cookies, Some(\"www.youtube.com.txt\"));\n    assert_eq!(proto.profile, Some(\"low-latency\"));\n    assert_eq!(proto.quality, Some(\"1080p\"));\n    assert_eq!(proto.v_codec, Some(\"av01\"));\n    assert_eq!(proto.v_title, Some(\"Title\".to_string()));\n    assert_eq!(proto.subfile, Some(\"http://example.com/en.ass\".to_string()));\n    assert_eq!(proto.startat, Some(\"233\"));\n    assert_eq!(proto.referrer, Some(\"https://www.youtube.com/\".to_string()));\n\n    // No parameter and last slash\n    let proto = Protocol::parse(\n        \"mpv-handler://play/aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj1HZ2tuMmY1ZS1JVQ\",\n    )\n    .unwrap();\n\n    assert_eq!(proto.scheme, Schemes::MpvHandler);\n    assert_eq!(proto.plugin, Plugins::Play);\n    assert_eq!(proto.url, \"https://www.youtube.com/watch?v=Ggkn2f5e-IU\");\n\n    // No parameter and protocol `mpv`\n    let proto = Protocol::parse(\n        \"mpv-handler://play/aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj1HZ2tuMmY1ZS1JVQ/\",\n    )\n    .unwrap();\n\n    assert_eq!(proto.scheme, Schemes::MpvHandler);\n    assert_eq!(proto.plugin, Plugins::Play);\n    assert_eq!(proto.url, \"https://www.youtube.com/watch?v=Ggkn2f5e-IU\");\n\n    // No parameter and protocol `mpv-handler-debug`\n    let proto = Protocol::parse(\n        \"mpv-handler-debug://play/aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj1HZ2tuMmY1ZS1JVQ\",\n    )\n    .unwrap();\n\n    assert_eq!(proto.scheme, Schemes::MpvHandlerDebug);\n    assert_eq!(proto.plugin, Plugins::Play);\n    assert_eq!(proto.url, \"https://www.youtube.com/watch?v=Ggkn2f5e-IU\");\n}\n"
  }
]