Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8da9aed4b | |||
| 954cfe9d4f | |||
| 31e9ecac35 | |||
| 0c3a7b70af | |||
| 254022d81d | |||
| b3ebef5ab9 | |||
| 0093df6016 | |||
| 30cbcf8ab1 | |||
| 7346730be9 | |||
| 59a057b14a | |||
| 2bc2c0bf03 | |||
| f13359df7f | |||
| ff14990bd8 | |||
| 0e62356183 | |||
| acec3c350e | |||
| fc520c1cc4 | |||
| 6137995fea | |||
| ae2e4eb155 | |||
| eb468b16df | |||
| 1be4d825fd | |||
| 01d039947a | |||
| 066b6bcbdb | |||
| b3273b7602 | |||
| d495a9851c | |||
| 6f5fd1d16e | |||
| f4b7049f4a | |||
| 4cccdcae77 | |||
| c21d08f050 | |||
| 00d3fb9212 | |||
| 7b12866334 | |||
| 1b415961cc | |||
| 74001462b4 | |||
| fdca1ab461 | |||
| 3d8ff2cedd | |||
| 9ef24f5a91 | |||
| 1314c14c59 | |||
| cb3a6a32cb | |||
| df56049db2 | |||
| 36a77ad8d1 | |||
| 71bce5d33e | |||
| b74dec7369 | |||
| d5c5f34d4c | |||
| 27be5c1b91 | |||
| 0c41d72ab2 | |||
| 25233349b9 | |||
| e04f6e4fdd | |||
| 24bcc56a8f | |||
| 45ad82bb66 | |||
| 13fcb5787d | |||
| 556e720574 | |||
| 791553bdc0 | |||
| 9361c608ca | |||
| 12729e2ca1 | |||
| b620112886 | |||
| cc1c80d367 | |||
| 63149c91a2 | |||
| 1e99d8b5c6 | |||
| b160d3c790 | |||
| d9cf5a5361 | |||
| 4f135f1153 | |||
| 4ee252f438 | |||
| 2fc08de757 | |||
| 6e3ca48d3f | |||
| 46a7777698 | |||
| 0f2174bf80 | |||
| 36fb34dc63 | |||
| 7f859db173 | |||
| 6e66105481 | |||
| 85b542983e | |||
| ecc6fd961a | |||
| 9260adc2d2 | |||
| cb6dfc1638 | |||
| 5dacd70287 | |||
| b163355c50 | |||
| 58495dd9fd | |||
| 1eb8a5ac0c | |||
| 452cd9e118 | |||
| 1345ac25f4 | |||
| ae8b610462 | |||
| 14297171be | |||
| 6f6c7563a0 | |||
| a52c2bb658 | |||
| 2ce400a5f7 | |||
| b8fd2d1762 | |||
| d2af0d11df | |||
| 57640d85d2 | |||
| d7b0ca8b3c | |||
| 8e6a1196b5 | |||
| c150124273 |
@@ -0,0 +1,2 @@
|
|||||||
|
ko_fi: afkarxyz
|
||||||
|
patreon: afkarxyz
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Bug Report
|
||||||
|
title: "[Bug Report] "
|
||||||
|
labels: ["bug"]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> **WARNING: Issues that do not follow this template will be deleted without review.**
|
||||||
|
>
|
||||||
|
> **Please keep `[Bug Report]` in the issue title and only continue after it.**
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem
|
||||||
|
placeholder: e.g. Downloading a playlist stops after the first track with no error message.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: type
|
||||||
|
attributes:
|
||||||
|
label: Type
|
||||||
|
description: Select the Spotify item type related to this bug.
|
||||||
|
options:
|
||||||
|
- Track
|
||||||
|
- Album
|
||||||
|
- Playlist
|
||||||
|
- Artist
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: spotify-url
|
||||||
|
attributes:
|
||||||
|
label: Spotify URL
|
||||||
|
placeholder: e.g. https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
placeholder: e.g. Happens every time on this link. Screenshot or recording attached.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "### Environment"
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: SpotiFLAC Version
|
||||||
|
placeholder: e.g. v7.1.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: OS
|
||||||
|
placeholder: e.g. Windows 11 23H2
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: location
|
||||||
|
attributes:
|
||||||
|
label: Location
|
||||||
|
placeholder: e.g. Indonesia
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Feature Request
|
||||||
|
title: "[Feature Request] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> **WARNING: Issues that do not follow this template will be deleted without review.**
|
||||||
|
>
|
||||||
|
> **Please keep `[Feature Request]` in the issue title and only continue after it.**
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
placeholder: e.g. Add an option to choose the output naming format for downloaded tracks.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: use-case
|
||||||
|
attributes:
|
||||||
|
label: Use Case
|
||||||
|
placeholder: e.g. I want downloaded files to follow a custom format like Artist - Title for easier library management.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
placeholder: e.g. Similar tools allow custom naming templates. Screenshot or mockup attached if needed.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: '1.25.5'
|
GO_VERSION: '1.26'
|
||||||
NODE_VERSION: '24'
|
NODE_VERSION: '24'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -175,8 +175,28 @@ jobs:
|
|||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
name: Build Linux
|
name: Build Linux (${{ matrix.display_name }})
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ${{ matrix.runner }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- display_name: amd64
|
||||||
|
runner: ubuntu-24.04
|
||||||
|
wails_platform: linux/amd64
|
||||||
|
artifact_name: linux-portable
|
||||||
|
output_name: SpotiFLAC.AppImage
|
||||||
|
appimage_arch: x86_64
|
||||||
|
appimagetool_arch: x86_64
|
||||||
|
pkgconfig_dir: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||||
|
- display_name: arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
wails_platform: linux/arm64
|
||||||
|
artifact_name: linux-portable-arm
|
||||||
|
output_name: SpotiFLAC-ARM.AppImage
|
||||||
|
appimage_arch: aarch64
|
||||||
|
appimagetool_arch: aarch64
|
||||||
|
pkgconfig_dir: /usr/lib/aarch64-linux-gnu/pkgconfig
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -225,7 +245,7 @@ jobs:
|
|||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libfuse2 imagemagick upx-ucl
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libfuse2 imagemagick upx-ucl
|
||||||
|
|
||||||
# Create symlink for webkit2gtk-4.0 -> webkit2gtk-4.1 (Ubuntu 24.04 compatibility)
|
# Create symlink for webkit2gtk-4.0 -> webkit2gtk-4.1 (Ubuntu 24.04 compatibility)
|
||||||
sudo ln -sf /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.1.pc /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.0.pc
|
sudo ln -sf "${{ matrix.pkgconfig_dir }}/webkit2gtk-4.1.pc" "${{ matrix.pkgconfig_dir }}/webkit2gtk-4.0.pc"
|
||||||
|
|
||||||
- name: Install Wails CLI
|
- name: Install Wails CLI
|
||||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
@@ -237,7 +257,7 @@ jobs:
|
|||||||
pnpm run generate-icon
|
pnpm run generate-icon
|
||||||
|
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run: wails build -platform linux/amd64
|
run: wails build -platform ${{ matrix.wails_platform }}
|
||||||
|
|
||||||
- name: Compress with UPX
|
- name: Compress with UPX
|
||||||
run: |
|
run: |
|
||||||
@@ -248,13 +268,13 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: appimagetool
|
path: appimagetool
|
||||||
key: appimagetool-x86_64-v1
|
key: appimagetool-${{ matrix.appimagetool_arch }}-v2
|
||||||
|
|
||||||
- name: Download appimagetool
|
- name: Download appimagetool
|
||||||
if: steps.cache-appimagetool.outputs.cache-hit != 'true'
|
if: steps.cache-appimagetool.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage || \
|
wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${{ matrix.appimagetool_arch }}.AppImage" || \
|
||||||
wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
|
wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-${{ matrix.appimagetool_arch }}.AppImage"
|
||||||
|
|
||||||
- name: Make appimagetool executable
|
- name: Make appimagetool executable
|
||||||
run: chmod +x appimagetool
|
run: chmod +x appimagetool
|
||||||
@@ -309,13 +329,13 @@ jobs:
|
|||||||
|
|
||||||
# Create AppImage
|
# Create AppImage
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
ARCH=x86_64 ./appimagetool --no-appstream AppDir dist/SpotiFLAC.AppImage
|
ARCH=${{ matrix.appimage_arch }} ./appimagetool --no-appstream AppDir "dist/${{ matrix.output_name }}"
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-portable
|
name: ${{ matrix.artifact_name }}
|
||||||
path: dist/SpotiFLAC.AppImage
|
path: dist/${{ matrix.output_name }}
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
create-release:
|
create-release:
|
||||||
@@ -356,7 +376,8 @@ jobs:
|
|||||||
|
|
||||||
- `SpotiFLAC.exe` - Windows
|
- `SpotiFLAC.exe` - Windows
|
||||||
- `SpotiFLAC.dmg` - macOS
|
- `SpotiFLAC.dmg` - macOS
|
||||||
- `SpotiFLAC.AppImage` - Linux
|
- `SpotiFLAC.AppImage` - Linux AMD64
|
||||||
|
- `SpotiFLAC-ARM.AppImage` - Linux ARM64
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Linux Requirements</b></summary>
|
<summary><b>Linux Requirements</b></summary>
|
||||||
@@ -384,10 +405,17 @@ jobs:
|
|||||||
./SpotiFLAC.AppImage
|
./SpotiFLAC.AppImage
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For ARM64:
|
||||||
|
```bash
|
||||||
|
chmod +x SpotiFLAC-ARM.AppImage
|
||||||
|
./SpotiFLAC-ARM.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
files: |
|
files: |
|
||||||
artifacts/windows-portable/*.exe
|
artifacts/windows-portable/SpotiFLAC.exe
|
||||||
artifacts/macos-portable/*.dmg
|
artifacts/macos-portable/SpotiFLAC.dmg
|
||||||
artifacts/linux-portable/*.AppImage
|
artifacts/linux-portable/SpotiFLAC.AppImage
|
||||||
|
artifacts/linux-portable-arm/SpotiFLAC-ARM.AppImage
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -61,4 +61,4 @@ test
|
|||||||
|
|
||||||
# Build notes (optional - uncomment if you don't want to commit)
|
# Build notes (optional - uncomment if you don't want to commit)
|
||||||
# BUILD_NOTES.md
|
# BUILD_NOTES.md
|
||||||
build.txt
|
push.bat
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 afkarxyz
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,27 +1,119 @@
|
|||||||
[](https://github.com/afkarxyz/SpotiFLAC/releases)
|
# SpotiFLAC
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/15737" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15737" alt="afkarxyz%2FSpotiFLAC | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account required.
|
Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account required.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
[](https://t.me/spotiflac)
|
||||||
|
[](https://t.me/spotiflac_chat)
|
||||||
|
|
||||||
</div>
|
### [Download](https://github.com/spotbye/SpotiFLAC/releases)
|
||||||
|
|
||||||
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases)
|

|
||||||
|
|
||||||
## Screenshot
|
## Other projects
|
||||||
|
|
||||||

|
### [SpotiFLAC Next](https://github.com/spotbye/SpotiFLAC-Next)
|
||||||
|
|
||||||
## Other project
|
Get Spotify tracks in true Lossless from Tidal, Qobuz, Amazon Music, Deezer & Apple Music — no account required.
|
||||||
|
|
||||||
### [SpotiDownloader](https://github.com/afkarxyz/SpotiDownloader)
|
### [SpotubeDL.com](https://spotubedl.com)
|
||||||
|
|
||||||
Get Spotify tracks in MP3 and FLAC via the spotidownloader.com API
|
Download Spotify Tracks, Albums, Playlists & Discography as MP3/OGG/Opus.
|
||||||
|
|
||||||
|
## Related projects
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>
|
||||||
|
> Related projects are maintained by the community and are not affiliated with the core SpotiFLAC desktop build.
|
||||||
|
|
||||||
|
### [SpotiFLAC (Mobile)](https://github.com/zarzet/SpotiFLAC-Mobile)
|
||||||
|
|
||||||
|
SpotiFLAC for Android & iOS — maintained by [@zarzet](https://github.com/zarzet)
|
||||||
|
|
||||||
|
### [SpotiFLAC (Python Module)](https://github.com/ShuShuzinhuu/SpotiFLAC-Module-Version)
|
||||||
|
|
||||||
|
SpotiFLAC Python library for SpotiFLAC integration — maintained by [@ShuShuzinhuu](https://github.com/ShuShuzinhuu)
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Is this software free?</summary>
|
||||||
|
|
||||||
|
_Yes. This software is completely free.
|
||||||
|
You do not need an account, login, or subscription.
|
||||||
|
All you need is an internet connection._
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Can using this software get my Spotify account suspended or banned?</summary>
|
||||||
|
|
||||||
|
_No.
|
||||||
|
This software has no connection to your Spotify account.
|
||||||
|
Spotify data is obtained through reverse engineering of the Spotify Web Player, not through user authentication._
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Where does the audio come from?</summary>
|
||||||
|
|
||||||
|
_The audio is fetched using third-party APIs._
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Why does metadata fetching sometimes fail?</summary>
|
||||||
|
|
||||||
|
_This usually happens because your IP address has been rate-limited.
|
||||||
|
You can wait and try again later, or use a VPN to bypass the rate limit._
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Why does Windows Defender or antivirus flag or delete the file?</summary>
|
||||||
|
|
||||||
|
_This is a false positive.
|
||||||
|
It likely happens because the executable is compressed using UPX._
|
||||||
|
|
||||||
|
_If you are concerned, you can fork the repository and build the software yourself from source._
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Want to support the project?</summary>
|
||||||
|
|
||||||
|
_If this software is useful and brings you value,
|
||||||
|
consider supporting the project by buying me a coffee.
|
||||||
|
Your support helps keep development going._
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
[](https://ko-fi.com/afkarxyz)
|
[](https://ko-fi.com/afkarxyz)
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This project is for **educational and private use only**. The developer does not condone or encourage copyright infringement.
|
||||||
|
|
||||||
|
**SpotiFLAC** is a third-party tool and is not affiliated with, endorsed by, or connected to Spotify, Tidal, Qobuz, Amazon Music or any other streaming service.
|
||||||
|
|
||||||
|
You are solely responsible for:
|
||||||
|
|
||||||
|
1. Ensuring your use of this software complies with your local laws.
|
||||||
|
2. Reading and adhering to the Terms of Service of the respective platforms.
|
||||||
|
3. Any legal consequences resulting from the misuse of this tool.
|
||||||
|
|
||||||
|
The software is provided "as is", without warranty of any kind. The author assumes no liability for any bans, damages, or legal issues arising from its use.
|
||||||
|
|
||||||
|
## API Credits
|
||||||
|
|
||||||
|
[MusicBrainz](https://musicbrainz.org) · [LRCLIB](https://lrclib.net) · [Songlink/Odesli](https://song.link) · [Songstats](https://songstats.com) · [hifi-api](https://github.com/binimum/hifi-api) · [Qobuz-DL](https://github.com/QobuzDL/Qobuz-DL)
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> **Star Us**, You will receive all release notifications from GitHub without any delay ~
|
||||||
|
|
||||||
|
[](https://repostars.dev/?repos=afkarxyz%2FSpotiFLAC&theme=forest)
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,30 +17,6 @@ import (
|
|||||||
type AmazonDownloader struct {
|
type AmazonDownloader struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
regions []string
|
regions []string
|
||||||
lastAPICallTime time.Time
|
|
||||||
apiCallCount int
|
|
||||||
apiCallResetTime time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type SongLinkResponse struct {
|
|
||||||
LinksByPlatform map[string]struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
} `json:"linksByPlatform"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DoubleDoubleSubmitResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DoubleDoubleStatusResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
FriendlyStatus string `json:"friendlyStatus"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
Current struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Artist string `json:"artist"`
|
|
||||||
} `json:"current"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAmazonDownloader() *AmazonDownloader {
|
func NewAmazonDownloader() *AmazonDownloader {
|
||||||
@@ -50,399 +25,381 @@ func NewAmazonDownloader() *AmazonDownloader {
|
|||||||
Timeout: 120 * time.Second,
|
Timeout: 120 * time.Second,
|
||||||
},
|
},
|
||||||
regions: []string{"us", "eu"},
|
regions: []string{"us", "eu"},
|
||||||
apiCallResetTime: time.Now(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) getRandomUserAgent() string {
|
|
||||||
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_%d_%d) AppleWebKit/%d.%d (KHTML, like Gecko) Chrome/%d.0.%d.%d Safari/%d.%d",
|
|
||||||
rand.Intn(4)+11, rand.Intn(5)+4,
|
|
||||||
rand.Intn(7)+530, rand.Intn(7)+30,
|
|
||||||
rand.Intn(25)+80, rand.Intn(1500)+3000, rand.Intn(65)+60,
|
|
||||||
rand.Intn(7)+530, rand.Intn(6)+30)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AmazonDownloader) GetAmazonURLFromSpotify(spotifyTrackID string) (string, error) {
|
func (a *AmazonDownloader) GetAmazonURLFromSpotify(spotifyTrackID string) (string, error) {
|
||||||
// Rate limiting: max 10 requests per minute (song.link API limit)
|
|
||||||
// Reset counter every minute
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(a.apiCallResetTime) >= time.Minute {
|
|
||||||
a.apiCallCount = 0
|
|
||||||
a.apiCallResetTime = now
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've hit the limit, wait until the next minute
|
|
||||||
if a.apiCallCount >= 9 { // Use 9 to be safe (limit is 10)
|
|
||||||
waitTime := time.Minute - now.Sub(a.apiCallResetTime)
|
|
||||||
if waitTime > 0 {
|
|
||||||
fmt.Printf("Rate limit reached, waiting %v...\n", waitTime.Round(time.Second))
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
a.apiCallCount = 0
|
|
||||||
a.apiCallResetTime = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add delay between requests (6 seconds = 10 requests per minute)
|
|
||||||
if !a.lastAPICallTime.IsZero() {
|
|
||||||
timeSinceLastCall := now.Sub(a.lastAPICallTime)
|
|
||||||
minDelay := 7 * time.Second // 7 seconds to be safe
|
|
||||||
if timeSinceLastCall < minDelay {
|
|
||||||
waitTime := minDelay - timeSinceLastCall
|
|
||||||
fmt.Printf("Rate limiting: waiting %v...\n", waitTime.Round(time.Second))
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 API URL
|
|
||||||
spotifyBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9vcGVuLnNwb3RpZnkuY29tL3RyYWNrLw==")
|
|
||||||
spotifyURL := fmt.Sprintf("%s%s", string(spotifyBase), spotifyTrackID)
|
|
||||||
|
|
||||||
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuc29uZy5saW5rL3YxLWFscGhhLjEvbGlua3M/dXJsPQ==")
|
|
||||||
apiURL := fmt.Sprintf("%s%s", string(apiBase), url.QueryEscape(spotifyURL))
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", apiURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("User-Agent", a.getRandomUserAgent())
|
|
||||||
|
|
||||||
fmt.Println("Getting Amazon URL...")
|
fmt.Println("Getting Amazon URL...")
|
||||||
|
client := NewSongLinkClient()
|
||||||
// Retry logic for rate limit errors
|
urls, err := client.GetAllURLsFromSpotify(spotifyTrackID, "")
|
||||||
maxRetries := 3
|
|
||||||
var resp *http.Response
|
|
||||||
for i := 0; i < maxRetries; i++ {
|
|
||||||
resp, err = a.client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get Amazon URL: %w", err)
|
return "", fmt.Errorf("failed to get Amazon URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update rate limit tracking
|
amazonURL := normalizeAmazonMusicURL(urls.AmazonURL)
|
||||||
a.lastAPICallTime = time.Now()
|
if amazonURL == "" {
|
||||||
a.apiCallCount++
|
|
||||||
|
|
||||||
if resp.StatusCode == 429 { // Too Many Requests
|
|
||||||
resp.Body.Close()
|
|
||||||
if i < maxRetries-1 {
|
|
||||||
waitTime := 15 * time.Second
|
|
||||||
fmt.Printf("Rate limited by API, waiting %v before retry...\n", waitTime)
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("API rate limit exceeded after %d retries", maxRetries)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
resp.Body.Close()
|
|
||||||
return "", fmt.Errorf("API returned status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Read body first to handle encoding issues and provide better error messages
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to read response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(body) == 0 {
|
|
||||||
return "", fmt.Errorf("API returned empty response")
|
|
||||||
}
|
|
||||||
|
|
||||||
var songLinkResp SongLinkResponse
|
|
||||||
if err := json.Unmarshal(body, &songLinkResp); err != nil {
|
|
||||||
// Truncate body for error message (max 200 chars)
|
|
||||||
bodyStr := string(body)
|
|
||||||
if len(bodyStr) > 200 {
|
|
||||||
bodyStr = bodyStr[:200] + "..."
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]
|
|
||||||
if !ok || amazonLink.URL == "" {
|
|
||||||
return "", fmt.Errorf("amazon Music link not found")
|
return "", fmt.Errorf("amazon Music link not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
amazonURL := amazonLink.URL
|
|
||||||
|
|
||||||
// Convert album URL to track URL if needed
|
|
||||||
if strings.Contains(amazonURL, "trackAsin=") {
|
|
||||||
parts := strings.Split(amazonURL, "trackAsin=")
|
|
||||||
if len(parts) > 1 {
|
|
||||||
trackAsin := strings.Split(parts[1], "&")[0]
|
|
||||||
musicBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9tdXNpYy5hbWF6b24uY29tL3RyYWNrcy8=")
|
|
||||||
amazonURL = fmt.Sprintf("%s%s?musicTerritory=US", string(musicBase), trackAsin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Found Amazon URL: %s\n", amazonURL)
|
fmt.Printf("Found Amazon URL: %s\n", amazonURL)
|
||||||
return amazonURL, nil
|
return amazonURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir string) (string, error) {
|
type amazonCommunityResponse struct {
|
||||||
var lastError error
|
ASIN string `json:"asin"`
|
||||||
|
Codec string `json:"codec"`
|
||||||
for _, region := range a.regions {
|
BitDepth int `json:"bit_depth"`
|
||||||
fmt.Printf("\nTrying region: %s...\n", region)
|
URL string `json:"url"`
|
||||||
// Decode base64 service URL
|
StreamURL string `json:"stream_url"`
|
||||||
serviceBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly8=")
|
Key string `json:"key"`
|
||||||
serviceDomain, _ := base64.StdEncoding.DecodeString("LmRvdWJsZWRvdWJsZS50b3A=")
|
KeySpecs []string `json:"key_specs"`
|
||||||
baseURL := fmt.Sprintf("%s%s%s", string(serviceBase), region, string(serviceDomain))
|
Captcha string `json:"captcha"`
|
||||||
|
|
||||||
// Step 1: Submit download request
|
|
||||||
encodedURL := url.QueryEscape(amazonURL)
|
|
||||||
submitURL := fmt.Sprintf("%s/dl?url=%s", baseURL, encodedURL)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", submitURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
lastError = fmt.Errorf("failed to create request: %w", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("User-Agent", a.getRandomUserAgent())
|
|
||||||
|
|
||||||
fmt.Println("Submitting download request...")
|
|
||||||
resp, err := a.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
lastError = fmt.Errorf("failed to submit request: %w", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
resp.Body.Close()
|
|
||||||
lastError = fmt.Errorf("submit failed with status %d", resp.StatusCode)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var submitResp DoubleDoubleSubmitResponse
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&submitResp); err != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
lastError = fmt.Errorf("failed to decode submit response: %w", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if !submitResp.Success || submitResp.ID == "" {
|
|
||||||
lastError = fmt.Errorf("submit request failed")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadID := submitResp.ID
|
|
||||||
fmt.Printf("Download ID: %s\n", downloadID)
|
|
||||||
|
|
||||||
// Step 2: Poll for completion
|
|
||||||
statusURL := fmt.Sprintf("%s/dl/%s", baseURL, downloadID)
|
|
||||||
fmt.Println("Waiting for download to complete...")
|
|
||||||
|
|
||||||
maxWait := 300 * time.Second
|
|
||||||
elapsed := time.Duration(0)
|
|
||||||
pollInterval := 3 * time.Second
|
|
||||||
|
|
||||||
for elapsed < maxWait {
|
|
||||||
time.Sleep(pollInterval)
|
|
||||||
elapsed += pollInterval
|
|
||||||
|
|
||||||
statusReq, err := http.NewRequest("GET", statusURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
statusReq.Header.Set("User-Agent", a.getRandomUserAgent())
|
|
||||||
|
|
||||||
statusResp, err := a.client.Do(statusReq)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("\rStatus check failed, retrying...")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if statusResp.StatusCode != 200 {
|
|
||||||
statusResp.Body.Close()
|
|
||||||
fmt.Printf("\rStatus check failed (status %d), retrying...", statusResp.StatusCode)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var status DoubleDoubleStatusResponse
|
|
||||||
if err := json.NewDecoder(statusResp.Body).Decode(&status); err != nil {
|
|
||||||
statusResp.Body.Close()
|
|
||||||
fmt.Printf("\rInvalid JSON response, retrying...")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
statusResp.Body.Close()
|
|
||||||
|
|
||||||
if status.Status == "done" {
|
|
||||||
fmt.Println("\nDownload ready!")
|
|
||||||
|
|
||||||
// Build download URL
|
|
||||||
fileURL := status.URL
|
|
||||||
if strings.HasPrefix(fileURL, "./") {
|
|
||||||
fileURL = fmt.Sprintf("%s/%s", baseURL, fileURL[2:])
|
|
||||||
} else if strings.HasPrefix(fileURL, "/") {
|
|
||||||
fileURL = fmt.Sprintf("%s%s", baseURL, fileURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
trackName := status.Current.Name
|
|
||||||
artist := status.Current.Artist
|
|
||||||
|
|
||||||
fmt.Printf("Downloading: %s - %s\n", artist, trackName)
|
|
||||||
|
|
||||||
// Download file
|
|
||||||
downloadReq, err := http.NewRequest("GET", fileURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
lastError = fmt.Errorf("failed to create download request: %w", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadReq.Header.Set("User-Agent", a.getRandomUserAgent())
|
|
||||||
|
|
||||||
fileResp, err := a.client.Do(downloadReq)
|
|
||||||
if err != nil {
|
|
||||||
lastError = fmt.Errorf("failed to download file: %w", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
defer fileResp.Body.Close()
|
|
||||||
|
|
||||||
if fileResp.StatusCode != 200 {
|
|
||||||
lastError = fmt.Errorf("download failed with status %d", fileResp.StatusCode)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate filename
|
|
||||||
fileName := fmt.Sprintf("%s - %s.flac", artist, trackName)
|
|
||||||
for _, char := range `<>:"/\|?*` {
|
|
||||||
fileName = strings.ReplaceAll(fileName, string(char), "")
|
|
||||||
}
|
|
||||||
fileName = strings.TrimSpace(fileName)
|
|
||||||
|
|
||||||
filePath := filepath.Join(outputDir, fileName)
|
|
||||||
|
|
||||||
// Save file
|
|
||||||
out, err := os.Create(filePath)
|
|
||||||
if err != nil {
|
|
||||||
lastError = fmt.Errorf("failed to create file: %w", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
fmt.Println("Downloading...")
|
|
||||||
// Use progress writer to track download
|
|
||||||
pw := NewProgressWriter(out)
|
|
||||||
_, err = io.Copy(pw, fileResp.Body)
|
|
||||||
if err != nil {
|
|
||||||
out.Close()
|
|
||||||
return "", fmt.Errorf("failed to write file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print final size
|
|
||||||
fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024))
|
|
||||||
fmt.Println("Download complete!")
|
|
||||||
return filePath, nil
|
|
||||||
|
|
||||||
} else if status.Status == "error" {
|
|
||||||
errorMsg := status.FriendlyStatus
|
|
||||||
if errorMsg == "" {
|
|
||||||
errorMsg = "Unknown error"
|
|
||||||
}
|
|
||||||
lastError = fmt.Errorf("processing failed: %s", errorMsg)
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
// Still processing
|
|
||||||
friendlyStatus := status.FriendlyStatus
|
|
||||||
if friendlyStatus == "" {
|
|
||||||
friendlyStatus = status.Status
|
|
||||||
}
|
|
||||||
fmt.Printf("\r%s...", friendlyStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if elapsed >= maxWait {
|
|
||||||
lastError = fmt.Errorf("download timeout")
|
|
||||||
fmt.Printf("\nError with %s region: %v\n", region, lastError)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastError != nil {
|
|
||||||
fmt.Printf("\nError with %s region: %v\n", region, lastError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("all regions failed. Last error: %v", lastError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyISRC string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool) (string, error) {
|
func amazonCommunityNormalizeQuality(quality string) string {
|
||||||
// Create output directory if needed
|
switch strings.ToLower(strings.TrimSpace(quality)) {
|
||||||
|
case "16", "lossless", "cd":
|
||||||
|
return "16"
|
||||||
|
case "atmos", "eac3", "dolby":
|
||||||
|
return "atmos"
|
||||||
|
default:
|
||||||
|
return "24"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AmazonDownloader) downloadFromCommunity(amazonURL, outputDir, quality string) (string, error) {
|
||||||
|
|
||||||
|
asinRegex := regexp.MustCompile(`(B[0-9A-Z]{9})`)
|
||||||
|
asin := asinRegex.FindString(amazonURL)
|
||||||
|
if asin == "" {
|
||||||
|
return "", fmt.Errorf("failed to extract ASIN from URL: %s", amazonURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(map[string]string{
|
||||||
|
"id": asin,
|
||||||
|
"quality": amazonCommunityNormalizeQuality(quality),
|
||||||
|
"country": "US",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Fetching from Amazon API (ASIN: %s)...\n", asin)
|
||||||
|
resp, err := doCommunityRequest(a.client, "Amazon", func() (*http.Request, error) {
|
||||||
|
req, err := NewRequestWithDefaultHeaders(http.MethodPost, GetAmazonCommunityDownloadURL(), bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if err := setCommunityRequestHeaders(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return "", fmt.Errorf("Amazon API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResp amazonCommunityResponse
|
||||||
|
if err := json.Unmarshal(bodyBytes, &apiResp); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
streamURL := strings.TrimSpace(apiResp.StreamURL)
|
||||||
|
if streamURL == "" {
|
||||||
|
streamURL = strings.TrimSpace(apiResp.URL)
|
||||||
|
}
|
||||||
|
if streamURL == "" {
|
||||||
|
return "", fmt.Errorf("no stream URL found in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
keySpecs := apiResp.KeySpecs
|
||||||
|
if len(keySpecs) == 0 {
|
||||||
|
if key := strings.TrimSpace(apiResp.Key); key != "" {
|
||||||
|
keySpecs = []string{key}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedPath := filepath.Join(outputDir, fmt.Sprintf("%s.encrypted.mp4", asin))
|
||||||
|
out, err := os.Create(encryptedPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
out.Close()
|
||||||
|
os.Remove(encryptedPath)
|
||||||
|
}()
|
||||||
|
|
||||||
|
dlReq, err := NewRequestWithDefaultHeaders(http.MethodGet, streamURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if captcha := strings.TrimSpace(apiResp.Captcha); captcha != "" {
|
||||||
|
dlReq.Header.Set("x-captcha-token", captcha)
|
||||||
|
}
|
||||||
|
|
||||||
|
dlResp, err := a.client.Do(dlReq)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer dlResp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Printf("Downloading track: %s\n", asin)
|
||||||
|
pw := NewProgressWriter(out)
|
||||||
|
if _, err = io.Copy(pw, dlResp.Body); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
out.Close()
|
||||||
|
|
||||||
|
fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024))
|
||||||
|
|
||||||
|
remuxInput := encryptedPath
|
||||||
|
if len(keySpecs) > 0 {
|
||||||
|
fmt.Printf("Decrypting file...\n")
|
||||||
|
decryptedPath := filepath.Join(outputDir, fmt.Sprintf("%s.decrypted.mp4", asin))
|
||||||
|
if err := decryptWithMP4FF(keySpecs, encryptedPath, decryptedPath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(decryptedPath)
|
||||||
|
remuxInput = decryptedPath
|
||||||
|
fmt.Println("Decryption successful")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetExt := ".flac"
|
||||||
|
if codec := strings.ToLower(strings.TrimSpace(apiResp.Codec)); codec == "eac3" || codec == "ec-3" || codec == "ac-3" {
|
||||||
|
targetExt = ".m4a"
|
||||||
|
}
|
||||||
|
finalPath := filepath.Join(outputDir, asin+targetExt)
|
||||||
|
|
||||||
|
if err := amazonRemuxWithFFmpeg(remuxInput, finalPath, targetExt); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info, err := os.Stat(finalPath); err != nil || info.Size() == 0 {
|
||||||
|
return "", fmt.Errorf("remuxed file missing or empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func amazonRemuxWithFFmpeg(inputPath, outputPath, targetExt string) error {
|
||||||
|
ffmpegPath, err := GetFFmpegPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ffmpeg not found for remux: %w", err)
|
||||||
|
}
|
||||||
|
if err := ValidateExecutable(ffmpegPath); err != nil {
|
||||||
|
return fmt.Errorf("invalid ffmpeg executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
runFFmpeg := func(args ...string) (string, error) {
|
||||||
|
cmd := exec.Command(ffmpegPath, args...)
|
||||||
|
setHideWindow(cmd)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
return string(output), err
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"-y", "-i", inputPath, "-map", "0:a:0", "-vn", "-c:a", "copy"}
|
||||||
|
if targetExt == ".m4a" {
|
||||||
|
args = append(args, "-f", "mp4")
|
||||||
|
}
|
||||||
|
args = append(args, outputPath)
|
||||||
|
|
||||||
|
if output, err := runFFmpeg(args...); err != nil {
|
||||||
|
if targetExt == ".flac" {
|
||||||
|
if output2, err2 := runFFmpeg("-y", "-i", inputPath, "-map", "0:a:0", "-vn", "-c:a", "flac", outputPath); err2 == nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
output = output2
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(output) > 500 {
|
||||||
|
output = output[len(output)-500:]
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ffmpeg remux failed: %v\nTail Output: %s", err, output)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir, quality string) (string, error) {
|
||||||
|
return a.downloadFromCommunity(amazonURL, outputDir, quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL string, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
||||||
|
|
||||||
if outputDir != "." {
|
if outputDir != "." {
|
||||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
return "", fmt.Errorf("failed to create output directory: %w", err)
|
return "", fmt.Errorf("failed to create output directory: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if file with expected name already exists (Amazon doesn't provide ISRC before download)
|
|
||||||
if spotifyTrackName != "" && spotifyArtistName != "" {
|
if spotifyTrackName != "" && spotifyArtistName != "" {
|
||||||
expectedFilename := BuildExpectedFilename(spotifyTrackName, spotifyArtistName, filenameFormat, includeTrackNumber, position, false)
|
filenameArtist := spotifyArtistName
|
||||||
|
filenameAlbumArtist := spotifyAlbumArtist
|
||||||
|
if useFirstArtistOnly {
|
||||||
|
filenameArtist = GetFirstArtist(spotifyArtistName)
|
||||||
|
filenameAlbumArtist = GetFirstArtist(spotifyAlbumArtist)
|
||||||
|
}
|
||||||
|
expectedFilename := BuildExpectedFilename(spotifyTrackName, filenameArtist, spotifyAlbumName, filenameAlbumArtist, spotifyReleaseDate, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyDiscNumber, false, isrcOverride)
|
||||||
expectedPath := filepath.Join(outputDir, expectedFilename)
|
expectedPath := filepath.Join(outputDir, expectedFilename)
|
||||||
|
|
||||||
|
if !GetRedownloadWithSuffixSetting() {
|
||||||
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 {
|
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 {
|
||||||
fmt.Printf("File already exists: %s (%.2f MB)\n", expectedPath, float64(fileInfo.Size())/(1024*1024))
|
fmt.Printf("File already exists: %s (%.2f MB)\n", expectedPath, float64(fileInfo.Size())/(1024*1024))
|
||||||
return "EXISTS:" + expectedPath, nil
|
return "EXISTS:" + expectedPath, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mbResult struct {
|
||||||
|
ISRC string
|
||||||
|
Metadata Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
metaChan := make(chan mbResult, 1)
|
||||||
|
if embedGenre && spotifyURL != "" {
|
||||||
|
go func() {
|
||||||
|
res := mbResult{}
|
||||||
|
var isrc string
|
||||||
|
parts := strings.Split(spotifyURL, "/")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
sID := strings.Split(parts[len(parts)-1], "?")[0]
|
||||||
|
if sID != "" {
|
||||||
|
client := NewSongLinkClient()
|
||||||
|
if val, err := client.GetISRC(sID); err == nil {
|
||||||
|
isrc = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.ISRC = isrc
|
||||||
|
if isrc != "" {
|
||||||
|
if ShouldSkipMusicBrainzMetadataFetch() {
|
||||||
|
fmt.Println("Skipping MusicBrainz metadata fetch because status check is offline.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Fetching MusicBrainz metadata...")
|
||||||
|
if fetchedMeta, err := FetchMusicBrainzMetadata(isrc, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useSingleGenre, embedGenre); err == nil {
|
||||||
|
res.Metadata = fetchedMeta
|
||||||
|
fmt.Println("MusicBrainz metadata fetched")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Warning: Failed to fetch MusicBrainz metadata: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metaChan <- res
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
close(metaChan)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Using Amazon URL: %s\n", amazonURL)
|
fmt.Printf("Using Amazon URL: %s\n", amazonURL)
|
||||||
|
|
||||||
// Download from service
|
filePath, err := a.DownloadFromService(amazonURL, outputDir, quality)
|
||||||
filePath, err := a.DownloadFromService(amazonURL, outputDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename file based on Spotify metadata
|
isrc := strings.TrimSpace(isrcOverride)
|
||||||
|
var mbMeta Metadata
|
||||||
|
if spotifyURL != "" {
|
||||||
|
result := <-metaChan
|
||||||
|
if isrc == "" {
|
||||||
|
isrc = result.ISRC
|
||||||
|
}
|
||||||
|
mbMeta = result.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
upc := ""
|
||||||
|
if spotifyURL != "" {
|
||||||
|
if identifiers, err := GetSpotifyTrackIdentifiersDirect(spotifyURL); err == nil || identifiers.ISRC != "" || identifiers.UPC != "" {
|
||||||
|
if strings.TrimSpace(isrc) == "" && strings.TrimSpace(identifiers.ISRC) != "" {
|
||||||
|
isrc = strings.TrimSpace(identifiers.ISRC)
|
||||||
|
}
|
||||||
|
upc = strings.TrimSpace(identifiers.UPC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
originalFileDir := filepath.Dir(filePath)
|
||||||
|
originalFileBase := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
|
||||||
|
|
||||||
if spotifyTrackName != "" && spotifyArtistName != "" {
|
if spotifyTrackName != "" && spotifyArtistName != "" {
|
||||||
safeArtist := sanitizeFilename(spotifyArtistName)
|
safeArtist := sanitizeFilename(spotifyArtistName)
|
||||||
safeTitle := sanitizeFilename(spotifyTrackName)
|
safeAlbumArtist := sanitizeFilename(spotifyAlbumArtist)
|
||||||
|
|
||||||
|
if useFirstArtistOnly {
|
||||||
|
safeArtist = sanitizeFilename(GetFirstArtist(spotifyArtistName))
|
||||||
|
safeAlbumArtist = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist))
|
||||||
|
}
|
||||||
|
|
||||||
|
safeTitle := sanitizeFilename(spotifyTrackName)
|
||||||
|
safeAlbum := sanitizeFilename(spotifyAlbumName)
|
||||||
|
|
||||||
|
year := ""
|
||||||
|
if len(spotifyReleaseDate) >= 4 {
|
||||||
|
year = spotifyReleaseDate[:4]
|
||||||
|
}
|
||||||
|
|
||||||
// Build filename based on format settings
|
|
||||||
var newFilename string
|
var newFilename string
|
||||||
|
|
||||||
// Check if format is a template (contains {})
|
|
||||||
if strings.Contains(filenameFormat, "{") {
|
if strings.Contains(filenameFormat, "{") {
|
||||||
newFilename = filenameFormat
|
newFilename = filenameFormat
|
||||||
newFilename = strings.ReplaceAll(newFilename, "{title}", safeTitle)
|
newFilename = strings.ReplaceAll(newFilename, "{title}", safeTitle)
|
||||||
newFilename = strings.ReplaceAll(newFilename, "{artist}", safeArtist)
|
newFilename = strings.ReplaceAll(newFilename, "{artist}", safeArtist)
|
||||||
|
newFilename = strings.ReplaceAll(newFilename, "{album}", safeAlbum)
|
||||||
|
newFilename = strings.ReplaceAll(newFilename, "{album_artist}", safeAlbumArtist)
|
||||||
|
newFilename = strings.ReplaceAll(newFilename, "{year}", year)
|
||||||
|
newFilename = strings.ReplaceAll(newFilename, "{date}", SanitizeFilename(spotifyReleaseDate))
|
||||||
|
newFilename = strings.ReplaceAll(newFilename, "{isrc}", SanitizeOptionalFilename(isrc))
|
||||||
|
|
||||||
|
if spotifyDiscNumber > 0 {
|
||||||
|
newFilename = strings.ReplaceAll(newFilename, "{disc}", fmt.Sprintf("%d", spotifyDiscNumber))
|
||||||
|
} else {
|
||||||
|
newFilename = strings.ReplaceAll(newFilename, "{disc}", "")
|
||||||
|
}
|
||||||
|
|
||||||
// Handle track number - if position is 0, remove {track} and surrounding separators
|
|
||||||
if position > 0 {
|
if position > 0 {
|
||||||
newFilename = strings.ReplaceAll(newFilename, "{track}", fmt.Sprintf("%02d", position))
|
newFilename = strings.ReplaceAll(newFilename, "{track}", fmt.Sprintf("%02d", position))
|
||||||
} else {
|
} else {
|
||||||
// Remove {track} with common separators
|
|
||||||
newFilename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(newFilename, "")
|
newFilename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(newFilename, "")
|
||||||
newFilename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(newFilename, "")
|
newFilename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(newFilename, "")
|
||||||
newFilename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(newFilename, "")
|
newFilename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(newFilename, "")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy format support
|
|
||||||
switch filenameFormat {
|
switch filenameFormat {
|
||||||
case "artist-title":
|
case "artist-title":
|
||||||
newFilename = fmt.Sprintf("%s - %s", safeArtist, safeTitle)
|
newFilename = fmt.Sprintf("%s - %s", safeArtist, safeTitle)
|
||||||
case "title":
|
case "title":
|
||||||
newFilename = safeTitle
|
newFilename = safeTitle
|
||||||
default: // "title-artist"
|
default:
|
||||||
newFilename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
newFilename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add track number prefix if enabled (legacy behavior)
|
|
||||||
if includeTrackNumber && position > 0 {
|
if includeTrackNumber && position > 0 {
|
||||||
newFilename = fmt.Sprintf("%02d. %s", position, newFilename)
|
newFilename = fmt.Sprintf("%02d. %s", position, newFilename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newFilename = newFilename + ".flac"
|
ext := filepath.Ext(filePath)
|
||||||
|
if ext == "" {
|
||||||
|
ext = ".flac"
|
||||||
|
}
|
||||||
|
newFilename = newFilename + ext
|
||||||
newFilePath := filepath.Join(outputDir, newFilename)
|
newFilePath := filepath.Join(outputDir, newFilename)
|
||||||
|
if GetRedownloadWithSuffixSetting() {
|
||||||
|
newFilePath, _ = ResolveOutputPathForDownload(newFilePath, true)
|
||||||
|
}
|
||||||
|
|
||||||
// Rename file
|
|
||||||
if err := os.Rename(filePath, newFilePath); err != nil {
|
if err := os.Rename(filePath, newFilePath); err != nil {
|
||||||
fmt.Printf("Warning: Failed to rename file: %v\n", err)
|
fmt.Printf("Warning: Failed to rename file: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -451,11 +408,10 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embed Spotify metadata (replace Amazon's embedded metadata)
|
|
||||||
fmt.Println("Embedding Spotify metadata...")
|
fmt.Println("Embedding Spotify metadata...")
|
||||||
|
|
||||||
coverPath := ""
|
coverPath := ""
|
||||||
// Download Spotify cover (with max resolution if enabled)
|
|
||||||
if spotifyCoverURL != "" {
|
if spotifyCoverURL != "" {
|
||||||
coverPath = filePath + ".cover.jpg"
|
coverPath = filePath + ".cover.jpg"
|
||||||
coverClient := NewCoverClient()
|
coverClient := NewCoverClient()
|
||||||
@@ -468,47 +424,64 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine track number to embed
|
|
||||||
// Use Spotify track number (album track number) if available, otherwise use position
|
|
||||||
trackNumberToEmbed := spotifyTrackNumber
|
trackNumberToEmbed := spotifyTrackNumber
|
||||||
if trackNumberToEmbed == 0 {
|
if trackNumberToEmbed == 0 {
|
||||||
trackNumberToEmbed = position // Fallback to playlist position
|
trackNumberToEmbed = 1
|
||||||
}
|
|
||||||
if trackNumberToEmbed == 0 {
|
|
||||||
trackNumberToEmbed = 1 // Default to track 1 for single track downloads without track number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build metadata from Spotify
|
|
||||||
metadata := Metadata{
|
metadata := Metadata{
|
||||||
Title: spotifyTrackName,
|
Title: spotifyTrackName,
|
||||||
Artist: spotifyArtistName,
|
Artist: spotifyArtistName,
|
||||||
Album: spotifyAlbumName,
|
Album: spotifyAlbumName,
|
||||||
AlbumArtist: spotifyAlbumArtist,
|
AlbumArtist: spotifyAlbumArtist,
|
||||||
Date: spotifyReleaseDate, // Recorded date (full date YYYY-MM-DD)
|
Date: spotifyReleaseDate,
|
||||||
TrackNumber: trackNumberToEmbed,
|
TrackNumber: trackNumberToEmbed,
|
||||||
TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify
|
TotalTracks: spotifyTotalTracks,
|
||||||
DiscNumber: spotifyDiscNumber, // Disc number from Spotify
|
DiscNumber: spotifyDiscNumber,
|
||||||
ISRC: spotifyISRC, // Use ISRC from Spotify
|
TotalDiscs: spotifyTotalDiscs,
|
||||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
URL: spotifyURL,
|
||||||
|
Comment: spotifyURL,
|
||||||
|
Copyright: spotifyCopyright,
|
||||||
|
Publisher: spotifyPublisher,
|
||||||
|
Composer: spotifyComposer,
|
||||||
|
Separator: metadataSeparator,
|
||||||
|
Description: "https://github.com/spotbye/SpotiFLAC",
|
||||||
|
ISRC: isrc,
|
||||||
|
UPC: upc,
|
||||||
|
Genre: mbMeta.Genre,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := EmbedMetadata(filePath, metadata, coverPath); err != nil {
|
if err := EmbedMetadataToConvertedFile(filePath, metadata, coverPath); err != nil {
|
||||||
fmt.Printf("Warning: Failed to embed metadata: %v\n", err)
|
fmt.Printf("Warning: Failed to embed metadata: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Metadata embedded successfully")
|
fmt.Println("Metadata embedded successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(strings.ToLower(filePath), ".flac") {
|
||||||
|
|
||||||
|
originalM4aPath := filepath.Join(originalFileDir, originalFileBase+".m4a")
|
||||||
|
if _, err := os.Stat(originalM4aPath); err == nil {
|
||||||
|
if err := os.Remove(originalM4aPath); err != nil {
|
||||||
|
fmt.Printf("Warning: Failed to remove M4A file: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Cleaned up original M4A file: %s\n", filepath.Base(originalM4aPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("Done")
|
fmt.Println("Done")
|
||||||
fmt.Println("✓ Downloaded successfully from Amazon Music")
|
fmt.Println("Downloaded successfully from Amazon Music")
|
||||||
return filePath, nil
|
return filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyISRC string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool) (string, error) {
|
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, quality, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL string,
|
||||||
// Get Amazon URL from Spotify track ID
|
useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool,
|
||||||
|
) (string, error) {
|
||||||
|
|
||||||
amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID)
|
amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.DownloadByURL(amazonURL, outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyISRC, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover)
|
return a.DownloadByURL(amazonURL, outputDir, quality, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL, useFirstArtistOnly, useSingleGenre, embedGenre)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"github.com/go-flac/go-flac"
|
"strconv"
|
||||||
mewflac "github.com/mewkiz/flac"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AnalysisResult contains the audio analysis data
|
|
||||||
type AnalysisResult struct {
|
type AnalysisResult struct {
|
||||||
FilePath string `json:"file_path"`
|
FilePath string `json:"file_path"`
|
||||||
FileSize int64 `json:"file_size"`
|
FileSize int64 `json:"file_size"`
|
||||||
@@ -18,172 +19,196 @@ type AnalysisResult struct {
|
|||||||
BitsPerSample uint8 `json:"bits_per_sample"`
|
BitsPerSample uint8 `json:"bits_per_sample"`
|
||||||
TotalSamples uint64 `json:"total_samples"`
|
TotalSamples uint64 `json:"total_samples"`
|
||||||
Duration float64 `json:"duration"`
|
Duration float64 `json:"duration"`
|
||||||
|
Bitrate int `json:"bit_rate"`
|
||||||
BitDepth string `json:"bit_depth"`
|
BitDepth string `json:"bit_depth"`
|
||||||
DynamicRange float64 `json:"dynamic_range"`
|
DynamicRange float64 `json:"dynamic_range"`
|
||||||
PeakAmplitude float64 `json:"peak_amplitude"`
|
PeakAmplitude float64 `json:"peak_amplitude"`
|
||||||
RMSLevel float64 `json:"rms_level"`
|
RMSLevel float64 `json:"rms_level"`
|
||||||
Spectrum *SpectrumData `json:"spectrum,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnalyzeTrack performs audio analysis on a FLAC file
|
type AnalysisDecodeResponse struct {
|
||||||
func AnalyzeTrack(filepath string) (*AnalysisResult, error) {
|
PCMBase64 string `json:"pcm_base64"`
|
||||||
|
SampleRate uint32 `json:"sample_rate"`
|
||||||
|
Channels uint8 `json:"channels"`
|
||||||
|
BitsPerSample uint8 `json:"bits_per_sample"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
|
BitrateKbps int `json:"bitrate_kbps,omitempty"`
|
||||||
|
BitDepth string `json:"bit_depth,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTrackMetadata(filepath string) (*AnalysisResult, error) {
|
||||||
if !fileExists(filepath) {
|
if !fileExists(filepath) {
|
||||||
return nil, fmt.Errorf("file does not exist: %s", filepath)
|
return nil, fmt.Errorf("file does not exist: %s", filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file size
|
return GetMetadataWithFFprobe(filepath)
|
||||||
fileInfo, err := os.Stat(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get file info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse FLAC file
|
|
||||||
f, err := flac.ParseFile(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse FLAC file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &AnalysisResult{
|
|
||||||
FilePath: filepath,
|
|
||||||
FileSize: fileInfo.Size(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract basic audio properties from STREAMINFO block
|
|
||||||
if len(f.Meta) > 0 {
|
|
||||||
streamInfo := f.Meta[0]
|
|
||||||
if streamInfo.Type == flac.StreamInfo {
|
|
||||||
// Read STREAMINFO data
|
|
||||||
data := streamInfo.Data
|
|
||||||
if len(data) >= 18 {
|
|
||||||
// Sample rate (bits 10-29 of bytes 10-13)
|
|
||||||
result.SampleRate = uint32(data[10])<<12 | uint32(data[11])<<4 | uint32(data[12])>>4
|
|
||||||
|
|
||||||
// Channels (bits 30-32 of byte 12)
|
|
||||||
result.Channels = ((data[12] >> 1) & 0x07) + 1
|
|
||||||
|
|
||||||
// Bits per sample (bits 33-37 of bytes 12-13)
|
|
||||||
result.BitsPerSample = ((data[12]&0x01)<<4 | data[13]>>4) + 1
|
|
||||||
|
|
||||||
// Total samples (bits 38-73 of bytes 13-17)
|
|
||||||
result.TotalSamples = uint64(data[13]&0x0F)<<32 |
|
|
||||||
uint64(data[14])<<24 |
|
|
||||||
uint64(data[15])<<16 |
|
|
||||||
uint64(data[16])<<8 |
|
|
||||||
uint64(data[17])
|
|
||||||
|
|
||||||
// Calculate duration
|
|
||||||
if result.SampleRate > 0 {
|
|
||||||
result.Duration = float64(result.TotalSamples) / float64(result.SampleRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read min/max frame size and block size for additional analysis
|
|
||||||
// Min block size (bytes 0-1)
|
|
||||||
// Max block size (bytes 2-3)
|
|
||||||
// These can give us hints about encoding quality
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Analyze spectrum and calculate real audio metrics
|
|
||||||
spectrum, err := AnalyzeSpectrum(filepath)
|
|
||||||
if err != nil {
|
|
||||||
// Log error but continue
|
|
||||||
fmt.Printf("Warning: failed to analyze spectrum: %v\n", err)
|
|
||||||
} else {
|
|
||||||
result.Spectrum = spectrum
|
|
||||||
// Calculate dynamic range, peak, and RMS from decoded samples
|
|
||||||
calculateRealAudioMetrics(result, filepath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set bit depth
|
|
||||||
result.BitDepth = fmt.Sprintf("%d-bit", result.BitsPerSample)
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateRealAudioMetrics calculates actual dynamic range, peak, and RMS from decoded audio
|
func GetMetadataWithFFprobe(filePath string) (*AnalysisResult, error) {
|
||||||
func calculateRealAudioMetrics(result *AnalysisResult, filepath string) {
|
ffprobePath, err := GetFFprobePath()
|
||||||
// Decode FLAC to get actual samples
|
|
||||||
samples, err := decodeFLACForMetrics(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate peak amplitude
|
|
||||||
var peak float64
|
|
||||||
var sumSquares float64
|
|
||||||
|
|
||||||
for _, sample := range samples {
|
|
||||||
absVal := sample
|
|
||||||
if absVal < 0 {
|
|
||||||
absVal = -absVal
|
|
||||||
}
|
|
||||||
if absVal > peak {
|
|
||||||
peak = absVal
|
|
||||||
}
|
|
||||||
sumSquares += sample * sample
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert peak to dB (reference: 1.0 = 0 dBFS)
|
|
||||||
peakDB := 20.0 * math.Log10(peak)
|
|
||||||
result.PeakAmplitude = peakDB
|
|
||||||
|
|
||||||
// Calculate RMS (Root Mean Square)
|
|
||||||
rms := math.Sqrt(sumSquares / float64(len(samples)))
|
|
||||||
rmsDB := 20.0 * math.Log10(rms)
|
|
||||||
result.RMSLevel = rmsDB
|
|
||||||
|
|
||||||
// Dynamic range is the difference between peak and RMS
|
|
||||||
result.DynamicRange = peakDB - rmsDB
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeFLACForMetrics decodes FLAC file and returns normalized samples for metric calculation
|
|
||||||
func decodeFLACForMetrics(filepath string) ([]float64, error) {
|
|
||||||
stream, err := mewflac.ParseFile(filepath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer stream.Close()
|
|
||||||
|
|
||||||
// Limit samples to prevent memory issues (10 million samples = ~3.8 minutes at 44.1kHz)
|
for i := 0; i < 5; i++ {
|
||||||
maxSamples := 10000000
|
if f, err := os.Open(filePath); err == nil {
|
||||||
samples := make([]float64, 0, maxSamples)
|
f.Close()
|
||||||
|
|
||||||
// Read all audio frames
|
|
||||||
for {
|
|
||||||
frame, err := stream.ParseNext()
|
|
||||||
if err != nil {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
// Get samples from first channel (mono or left channel)
|
|
||||||
var channelSamples []int32
|
|
||||||
if len(frame.Subframes) > 0 {
|
|
||||||
channelSamples = frame.Subframes[0].Samples
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize samples to -1.0 to 1.0 range
|
args := []string{
|
||||||
maxVal := float64(int64(1) << (stream.Info.BitsPerSample - 1))
|
"-v", "error",
|
||||||
for _, sample := range channelSamples {
|
"-select_streams", "a:0",
|
||||||
if len(samples) >= maxSamples {
|
"-show_entries", "stream=sample_rate,channels,bits_per_raw_sample,bits_per_sample,duration,bit_rate",
|
||||||
return samples, nil
|
"-of", "default=noprint_wrappers=0",
|
||||||
|
filePath,
|
||||||
}
|
}
|
||||||
normalized := float64(sample) / maxVal
|
cmd := exec.Command(ffprobePath, args...)
|
||||||
samples = append(samples, normalized)
|
setHideWindow(cmd)
|
||||||
}
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
if len(samples) >= maxSamples {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return samples, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFileSize(filepath string) (int64, error) {
|
|
||||||
info, err := os.Stat(filepath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, fmt.Errorf("ffprobe failed: %v - %s", err, string(output))
|
||||||
}
|
}
|
||||||
return info.Size(), nil
|
|
||||||
|
infoMap := make(map[string]string)
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "=") {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
infoMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &AnalysisResult{
|
||||||
|
FilePath: filePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if info, err := os.Stat(filePath); err == nil {
|
||||||
|
res.FileSize = info.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := infoMap["sample_rate"]; ok {
|
||||||
|
s, _ := strconv.Atoi(val)
|
||||||
|
res.SampleRate = uint32(s)
|
||||||
|
}
|
||||||
|
if val, ok := infoMap["channels"]; ok {
|
||||||
|
c, _ := strconv.Atoi(val)
|
||||||
|
res.Channels = uint8(c)
|
||||||
|
}
|
||||||
|
if val, ok := infoMap["duration"]; ok {
|
||||||
|
d, _ := strconv.ParseFloat(val, 64)
|
||||||
|
res.Duration = d
|
||||||
|
}
|
||||||
|
if val, ok := infoMap["bit_rate"]; ok && val != "N/A" {
|
||||||
|
br, _ := strconv.Atoi(val)
|
||||||
|
res.Bitrate = br
|
||||||
|
}
|
||||||
|
|
||||||
|
bits := 0
|
||||||
|
if val, ok := infoMap["bits_per_raw_sample"]; ok && val != "N/A" {
|
||||||
|
bits, _ = strconv.Atoi(val)
|
||||||
|
}
|
||||||
|
if bits == 0 {
|
||||||
|
if val, ok := infoMap["bits_per_sample"]; ok && val != "N/A" {
|
||||||
|
bits, _ = strconv.Atoi(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.BitsPerSample = uint8(bits)
|
||||||
|
if bits > 0 {
|
||||||
|
res.BitDepth = fmt.Sprintf("%d-bit", bits)
|
||||||
|
} else {
|
||||||
|
res.BitDepth = "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeAudioForAnalysis(filePath string) (*AnalysisDecodeResponse, error) {
|
||||||
|
metadata, err := GetTrackMetadata(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pcmBase64, err := extractAnalysisPCMBase64(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &AnalysisDecodeResponse{
|
||||||
|
PCMBase64: pcmBase64,
|
||||||
|
SampleRate: metadata.SampleRate,
|
||||||
|
Channels: metadata.Channels,
|
||||||
|
BitsPerSample: metadata.BitsPerSample,
|
||||||
|
Duration: metadata.Duration,
|
||||||
|
BitDepth: metadata.BitDepth,
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.Bitrate > 0 {
|
||||||
|
resp.BitrateKbps = metadata.Bitrate / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractAnalysisPCMBase64(filePath string) (string, error) {
|
||||||
|
ffmpegPath, err := GetFFmpegPath()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
argSets := [][]string{
|
||||||
|
{
|
||||||
|
"-v", "error",
|
||||||
|
"-i", filePath,
|
||||||
|
"-vn",
|
||||||
|
"-map", "0:a:0",
|
||||||
|
"-af", "pan=mono|c0=c0",
|
||||||
|
"-f", "s16le",
|
||||||
|
"-acodec", "pcm_s16le",
|
||||||
|
"pipe:1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"-v", "error",
|
||||||
|
"-i", filePath,
|
||||||
|
"-vn",
|
||||||
|
"-map", "0:a:0",
|
||||||
|
"-ac", "1",
|
||||||
|
"-f", "s16le",
|
||||||
|
"-acodec", "pcm_s16le",
|
||||||
|
"pipe:1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
for _, args := range argSets {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
cmd := exec.Command(ffmpegPath, args...)
|
||||||
|
setHideWindow(cmd)
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
lastErr = fmt.Errorf("ffmpeg analysis decode failed: %w - %s", err, strings.TrimSpace(stderr.String()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if stdout.Len() == 0 {
|
||||||
|
lastErr = fmt.Errorf("ffmpeg analysis decode returned empty PCM output")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(stdout.Bytes()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil {
|
||||||
|
return "", lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("ffmpeg analysis decode failed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func normalizeArtistSeparator(separator string) string {
|
||||||
|
separator = strings.TrimSpace(separator)
|
||||||
|
if separator == "," || separator == ";" {
|
||||||
|
return separator
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitArtistSegment(segment string, separator string) []string {
|
||||||
|
segment = strings.TrimSpace(segment)
|
||||||
|
if segment == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(segment, "|||SEP|||") {
|
||||||
|
return strings.Split(segment, "|||SEP|||")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := []string{segment}
|
||||||
|
|
||||||
|
if separator = normalizeArtistSeparator(separator); separator != "" {
|
||||||
|
var separated []string
|
||||||
|
for _, part := range parts {
|
||||||
|
for _, item := range strings.Split(part, separator) {
|
||||||
|
separated = append(separated, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = separated
|
||||||
|
} else if strings.Contains(segment, ";") {
|
||||||
|
var separated []string
|
||||||
|
for _, part := range parts {
|
||||||
|
for _, item := range strings.Split(part, ";") {
|
||||||
|
separated = append(separated, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = separated
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitArtistCredits(artistStr, separator string) []string {
|
||||||
|
rawParts := splitArtistSegment(artistStr, separator)
|
||||||
|
if len(rawParts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(rawParts))
|
||||||
|
result := make([]string, 0, len(rawParts))
|
||||||
|
for _, part := range rawParts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[part]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[part] = struct{}{}
|
||||||
|
result = append(result, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitMetadataValues(value, separator string) []string {
|
||||||
|
rawParts := splitArtistSegment(value, separator)
|
||||||
|
if len(rawParts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(rawParts))
|
||||||
|
result := make([]string, 0, len(rawParts))
|
||||||
|
for _, part := range rawParts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[part]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[part] = struct{}{}
|
||||||
|
result = append(result, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
communityAPIKeyOnce sync.Once
|
||||||
|
communityAPIKey string
|
||||||
|
communityAPIKeyErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
var communityAPIKeySeedParts = [][]byte{
|
||||||
|
[]byte("spotif"),
|
||||||
|
[]byte("lac:co"),
|
||||||
|
[]byte("mmunity:apikey:v1"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var communityAPIKeyAAD = []byte("spotiflac|community|apikey|v1")
|
||||||
|
|
||||||
|
var communityAPIKeyNonce = []byte{
|
||||||
|
0x20, 0x5c, 0x92, 0x4b, 0x61, 0xc2, 0x79, 0xd3, 0xea, 0x5d, 0xdd, 0xd4,
|
||||||
|
}
|
||||||
|
|
||||||
|
var communityAPIKeyCiphertext = []byte{
|
||||||
|
0x51, 0x0b, 0x26, 0xaf, 0xac, 0x6f, 0xf6, 0x41, 0x79, 0xde, 0x8d, 0x36,
|
||||||
|
0x83, 0x46, 0xb5, 0xd5, 0x96, 0xef, 0xad, 0xed, 0xe0, 0xd0, 0xc7, 0xc2,
|
||||||
|
0x90, 0x01, 0x50, 0x5f, 0x55, 0x59, 0x9f, 0xac, 0x1f, 0xd0, 0x70, 0x18,
|
||||||
|
0x91, 0x4f, 0x7a, 0x32,
|
||||||
|
}
|
||||||
|
|
||||||
|
var communityAPIKeyTag = []byte{
|
||||||
|
0x56, 0xb0, 0x28, 0x68, 0x9f, 0x39, 0x0d, 0xbc, 0xc0, 0x8e, 0xfb, 0x52,
|
||||||
|
0x3a, 0xd6, 0x18, 0xae,
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommunityAPIKey() (string, error) {
|
||||||
|
communityAPIKeyOnce.Do(func() {
|
||||||
|
hasher := sha256.New()
|
||||||
|
for _, part := range communityAPIKeySeedParts {
|
||||||
|
hasher.Write(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(hasher.Sum(nil))
|
||||||
|
if err != nil {
|
||||||
|
communityAPIKeyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
communityAPIKeyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed := make([]byte, 0, len(communityAPIKeyCiphertext)+len(communityAPIKeyTag))
|
||||||
|
sealed = append(sealed, communityAPIKeyCiphertext...)
|
||||||
|
sealed = append(sealed, communityAPIKeyTag...)
|
||||||
|
|
||||||
|
plaintext, err := gcm.Open(nil, communityAPIKeyNonce, sealed, communityAPIKeyAAD)
|
||||||
|
if err != nil {
|
||||||
|
communityAPIKeyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
communityAPIKey = string(plaintext)
|
||||||
|
})
|
||||||
|
|
||||||
|
if communityAPIKeyErr != nil {
|
||||||
|
return "", communityAPIKeyErr
|
||||||
|
}
|
||||||
|
return communityAPIKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func communityUserAgent() string {
|
||||||
|
version := strings.TrimSpace(AppVersion)
|
||||||
|
if version == "" || version == "Unknown" {
|
||||||
|
return "SpotiFLAC"
|
||||||
|
}
|
||||||
|
return "SpotiFLAC/" + version
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCommunityRequestHeaders(req *http.Request) error {
|
||||||
|
apiKey, err := getCommunityAPIKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to prepare community API key: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("x-api-key", apiKey)
|
||||||
|
req.Header.Set("User-Agent", communityUserAgent())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const communityDownloadPath = "/api/dl"
|
||||||
|
|
||||||
|
var communityURLSeedParts = [][]byte{
|
||||||
|
[]byte("spotif"),
|
||||||
|
[]byte("lac:co"),
|
||||||
|
[]byte("mmunity:url:v1"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var communityURLAAD = []byte("spotiflac|community|url|v1")
|
||||||
|
|
||||||
|
var (
|
||||||
|
tidalCommunityURLNonce = []byte{
|
||||||
|
0x6a, 0x2a, 0x9e, 0xf3, 0x25, 0x5f, 0x48, 0x3c, 0xc3, 0xdf, 0x1d, 0xa9,
|
||||||
|
}
|
||||||
|
tidalCommunityURLCiphertext = []byte{
|
||||||
|
0x8f, 0x90, 0xa4, 0x28, 0x24, 0x06, 0x35, 0x13, 0x2d, 0x33, 0x96, 0x9a,
|
||||||
|
0xd7, 0x2c, 0x31, 0x42, 0x6a, 0xf3, 0xee, 0x86, 0x34, 0x99, 0x15, 0x1e,
|
||||||
|
0xa9, 0x07, 0x06, 0xe6, 0xee, 0x0d, 0x75,
|
||||||
|
}
|
||||||
|
tidalCommunityURLTag = []byte{
|
||||||
|
0x4d, 0x1c, 0x4e, 0x98, 0x96, 0x07, 0x16, 0xad, 0x6a, 0x7c, 0xa0, 0xdf,
|
||||||
|
0xe9, 0xc5, 0xf6, 0x87,
|
||||||
|
}
|
||||||
|
|
||||||
|
qobuzCommunityURLNonce = []byte{
|
||||||
|
0x5f, 0xd8, 0xfd, 0xfd, 0x89, 0x83, 0xe7, 0x6c, 0xde, 0x48, 0x47, 0x8d,
|
||||||
|
}
|
||||||
|
qobuzCommunityURLCiphertext = []byte{
|
||||||
|
0xfa, 0x35, 0x21, 0xba, 0x02, 0xc6, 0x15, 0x1f, 0x0e, 0xa3, 0xa6, 0x16,
|
||||||
|
0x64, 0x2b, 0xd8, 0xfb, 0xf5, 0x35, 0xfe, 0xe9, 0x0e, 0x59, 0xd9, 0x25,
|
||||||
|
0x72, 0x57, 0x88, 0x94, 0xa9, 0xb7, 0x70,
|
||||||
|
}
|
||||||
|
qobuzCommunityURLTag = []byte{
|
||||||
|
0xd7, 0x72, 0xb5, 0x2b, 0x1c, 0xb1, 0xfd, 0xba, 0x22, 0x09, 0x25, 0x41,
|
||||||
|
0x87, 0x85, 0x30, 0x1b,
|
||||||
|
}
|
||||||
|
|
||||||
|
amazonCommunityURLNonce = []byte{
|
||||||
|
0x55, 0x18, 0x01, 0x42, 0x42, 0x0c, 0xf6, 0x78, 0x8a, 0x73, 0xd7, 0x63,
|
||||||
|
}
|
||||||
|
amazonCommunityURLCiphertext = []byte{
|
||||||
|
0xd2, 0xf3, 0xdc, 0xe8, 0x62, 0xf0, 0xad, 0xc2, 0x4a, 0x43, 0xb1, 0xa2,
|
||||||
|
0x1c, 0x0d, 0x41, 0x3e, 0x2e, 0x30, 0x29, 0x5e, 0x46, 0xe2, 0xc2, 0xd6,
|
||||||
|
0xc1, 0xf3, 0xe3, 0x1a, 0x8f, 0x67, 0xfe,
|
||||||
|
}
|
||||||
|
amazonCommunityURLTag = []byte{
|
||||||
|
0xf9, 0x0a, 0xfd, 0xed, 0x9e, 0xe8, 0xb4, 0xc0, 0x75, 0xf3, 0xd5, 0x74,
|
||||||
|
0x3c, 0xb6, 0xa1, 0xb9,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
communityURLGCMOnce sync.Once
|
||||||
|
communityURLGCM cipher.AEAD
|
||||||
|
communityURLGCMErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func communityURLCipher() (cipher.AEAD, error) {
|
||||||
|
communityURLGCMOnce.Do(func() {
|
||||||
|
hasher := sha256.New()
|
||||||
|
for _, part := range communityURLSeedParts {
|
||||||
|
hasher.Write(part)
|
||||||
|
}
|
||||||
|
block, err := aes.NewCipher(hasher.Sum(nil))
|
||||||
|
if err != nil {
|
||||||
|
communityURLGCMErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
communityURLGCMErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
communityURLGCM = gcm
|
||||||
|
})
|
||||||
|
return communityURLGCM, communityURLGCMErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptCommunityURL(nonce, ciphertext, tag []byte) (string, error) {
|
||||||
|
gcm, err := communityURLCipher()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
sealed := make([]byte, 0, len(ciphertext)+len(tag))
|
||||||
|
sealed = append(sealed, ciphertext...)
|
||||||
|
sealed = append(sealed, tag...)
|
||||||
|
plaintext, err := gcm.Open(nil, nonce, sealed, communityURLAAD)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(plaintext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const communityRateLimitMaxRetries = 6
|
||||||
|
|
||||||
|
const communityRateLimitFallbackWait = 30 * time.Second
|
||||||
|
|
||||||
|
func GetTidalCommunityDownloadURL() string {
|
||||||
|
base, _ := decryptCommunityURL(tidalCommunityURLNonce, tidalCommunityURLCiphertext, tidalCommunityURLTag)
|
||||||
|
return base + communityDownloadPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzCommunityDownloadURL() string {
|
||||||
|
base, _ := decryptCommunityURL(qobuzCommunityURLNonce, qobuzCommunityURLCiphertext, qobuzCommunityURLTag)
|
||||||
|
return base + communityDownloadPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAmazonCommunityDownloadURL() string {
|
||||||
|
base, _ := decryptCommunityURL(amazonCommunityURLNonce, amazonCommunityURLCiphertext, amazonCommunityURLTag)
|
||||||
|
return base + communityDownloadPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func communityRetryAfter(resp *http.Response) time.Duration {
|
||||||
|
if resp == nil {
|
||||||
|
return communityRateLimitFallbackWait
|
||||||
|
}
|
||||||
|
if ra := strings.TrimSpace(resp.Header.Get("Retry-After")); ra != "" {
|
||||||
|
if secs, err := strconv.Atoi(ra); err == nil && secs >= 0 {
|
||||||
|
return time.Duration(secs)*time.Second + 250*time.Millisecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reset := strings.TrimSpace(resp.Header.Get("X-RateLimit-Reset")); reset != "" {
|
||||||
|
if epoch, err := strconv.ParseInt(reset, 10, 64); err == nil {
|
||||||
|
if wait := time.Until(time.Unix(epoch, 0)); wait > 0 {
|
||||||
|
return wait + 250*time.Millisecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return communityRateLimitFallbackWait
|
||||||
|
}
|
||||||
|
|
||||||
|
func doCommunityRequest(client *http.Client, service string, reqFn func() (*http.Request, error)) (*http.Response, error) {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt <= communityRateLimitMaxRetries; attempt++ {
|
||||||
|
req, err := reqFn()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusTooManyRequests {
|
||||||
|
ClearRateLimitCooldown()
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wait := communityRetryAfter(resp)
|
||||||
|
resp.Body.Close()
|
||||||
|
lastErr = fmt.Errorf("%s community API rate limited (429)", service)
|
||||||
|
|
||||||
|
if attempt == communityRateLimitMaxRetries {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Printf("%s rate limited, waiting %.0fs before retry (%d/%d)...\n", service, wait.Seconds(), attempt+1, communityRateLimitMaxRetries)
|
||||||
|
SetRateLimitCooldown(wait.Seconds())
|
||||||
|
if sleepErr := SleepWithDownloadContext(wait); sleepErr != nil {
|
||||||
|
ClearRateLimitCooldown()
|
||||||
|
return nil, sleepErr
|
||||||
|
}
|
||||||
|
ClearRateLimitCooldown()
|
||||||
|
}
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
@@ -1,18 +1,249 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const legacyTidalAPICacheFile = "tidal-api-urls.json"
|
||||||
|
|
||||||
|
func normalizeCustomTidalAPIValue(value interface{}) string {
|
||||||
|
customAPI, _ := value.(string)
|
||||||
|
customAPI = strings.TrimRight(strings.TrimSpace(customAPI), "/")
|
||||||
|
if strings.HasPrefix(customAPI, "https://") {
|
||||||
|
return customAPI
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeDownloaderValue(value interface{}, allowTidal bool) string {
|
||||||
|
downloader, _ := value.(string)
|
||||||
|
switch strings.TrimSpace(strings.ToLower(downloader)) {
|
||||||
|
case "tidal":
|
||||||
|
if allowTidal {
|
||||||
|
return "tidal"
|
||||||
|
}
|
||||||
|
return "auto"
|
||||||
|
case "qobuz":
|
||||||
|
return "qobuz"
|
||||||
|
case "amazon":
|
||||||
|
return "amazon"
|
||||||
|
default:
|
||||||
|
return "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeAutoOrderValue(value interface{}, allowTidal bool) string {
|
||||||
|
autoOrder, _ := value.(string)
|
||||||
|
allowed := map[string]struct{}{
|
||||||
|
"qobuz": {},
|
||||||
|
"amazon": {},
|
||||||
|
}
|
||||||
|
fallback := "qobuz-amazon"
|
||||||
|
if allowTidal {
|
||||||
|
allowed["tidal"] = struct{}{}
|
||||||
|
fallback = "tidal-qobuz-amazon"
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
parts := make([]string, 0, 3)
|
||||||
|
for _, rawPart := range strings.Split(strings.TrimSpace(strings.ToLower(autoOrder)), "-") {
|
||||||
|
part := strings.TrimSpace(rawPart)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := allowed[part]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[part]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[part] = struct{}{}
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizeSettingsMap(settings map[string]interface{}) map[string]interface{} {
|
||||||
|
if settings == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized := make(map[string]interface{}, len(settings))
|
||||||
|
for key, value := range settings {
|
||||||
|
sanitized[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
customAPI := normalizeCustomTidalAPIValue(sanitized["customTidalApi"])
|
||||||
|
sanitized["customTidalApi"] = customAPI
|
||||||
|
allowTidal := customAPI != ""
|
||||||
|
sanitized["downloader"] = sanitizeDownloaderValue(sanitized["downloader"], allowTidal)
|
||||||
|
sanitized["autoOrder"] = sanitizeAutoOrderValue(sanitized["autoOrder"], allowTidal)
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanupLegacyTidalPublicAPIState() error {
|
||||||
|
appDir, err := EnsureAppDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cachePath := filepath.Join(appDir, legacyTidalAPICacheFile)
|
||||||
|
if err := os.Remove(cachePath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizePersistedConfigSettings() error {
|
||||||
|
configPath, err := GetConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &settings); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized := SanitizeSettingsMap(settings)
|
||||||
|
payload, err := json.MarshalIndent(sanitized, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(configPath, payload, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
func GetDefaultMusicPath() string {
|
func GetDefaultMusicPath() string {
|
||||||
// Get user's home directory
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to Public Music if can't get home dir
|
|
||||||
return "C:\\Users\\Public\\Music"
|
return "C:\\Users\\Public\\Music"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return path to user's Music folder
|
|
||||||
return filepath.Join(homeDir, "Music")
|
return filepath.Join(homeDir, "Music")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetConfigPath() (string, error) {
|
||||||
|
dir, err := EnsureAppDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(dir, "config.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfigSettings() (map[string]interface{}, error) {
|
||||||
|
configPath, err := GetConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &settings); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return SanitizeSettingsMap(settings), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRedownloadWithSuffixSetting() bool {
|
||||||
|
settings, err := LoadConfigSettings()
|
||||||
|
if err != nil || settings == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled, _ := settings["redownloadWithSuffix"].(bool)
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCustomTidalAPISetting() string {
|
||||||
|
settings, err := LoadConfigSettings()
|
||||||
|
if err != nil || settings == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeCustomTidalAPIValue(settings["customTidalApi"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeExistingFileCheckMode(value string) string {
|
||||||
|
switch strings.TrimSpace(strings.ToLower(value)) {
|
||||||
|
case "isrc", "upc":
|
||||||
|
return "isrc"
|
||||||
|
default:
|
||||||
|
return "filename"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetExistingFileCheckModeSetting() string {
|
||||||
|
settings, err := LoadConfigSettings()
|
||||||
|
if err != nil || settings == nil {
|
||||||
|
return "filename"
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMode, _ := settings["existingFileCheckMode"].(string)
|
||||||
|
return normalizeExistingFileCheckMode(rawMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLinkResolverSetting() string {
|
||||||
|
settings, err := LoadConfigSettings()
|
||||||
|
if err != nil || settings == nil {
|
||||||
|
return linkResolverProviderDeezerSongLink
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver, _ := settings["linkResolver"].(string)
|
||||||
|
switch strings.TrimSpace(strings.ToLower(resolver)) {
|
||||||
|
case "songlink", linkResolverProviderDeezerSongLink:
|
||||||
|
return linkResolverProviderDeezerSongLink
|
||||||
|
case "songstats":
|
||||||
|
return linkResolverProviderSongstats
|
||||||
|
case "":
|
||||||
|
return linkResolverProviderDeezerSongLink
|
||||||
|
default:
|
||||||
|
return linkResolverProviderDeezerSongLink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLinkResolverAllowFallback() bool {
|
||||||
|
settings, err := LoadConfigSettings()
|
||||||
|
if err != nil || settings == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
allowFallback, ok := settings["allowResolverFallback"].(bool)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowFallback
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -9,26 +12,31 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
xdraw "golang.org/x/image/draw"
|
||||||
|
_ "image/jpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Spotify image size codes
|
spotifySize300 = "ab67616d00001e02"
|
||||||
spotifySize640 = "ab67616d0000b273" // 640x640
|
spotifySize640 = "ab67616d0000b273"
|
||||||
spotifySizeMax = "ab67616d000082c1" // Max resolution
|
spotifySizeMax = "ab67616d000082c1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CoverDownloadRequest represents a request to download cover art
|
|
||||||
type CoverDownloadRequest struct {
|
type CoverDownloadRequest struct {
|
||||||
CoverURL string `json:"cover_url"`
|
CoverURL string `json:"cover_url"`
|
||||||
TrackName string `json:"track_name"`
|
TrackName string `json:"track_name"`
|
||||||
ArtistName string `json:"artist_name"`
|
ArtistName string `json:"artist_name"`
|
||||||
|
AlbumName string `json:"album_name"`
|
||||||
|
AlbumArtist string `json:"album_artist"`
|
||||||
|
ReleaseDate string `json:"release_date"`
|
||||||
OutputDir string `json:"output_dir"`
|
OutputDir string `json:"output_dir"`
|
||||||
FilenameFormat string `json:"filename_format"`
|
FilenameFormat string `json:"filename_format"`
|
||||||
TrackNumber bool `json:"track_number"`
|
TrackNumber bool `json:"track_number"`
|
||||||
Position int `json:"position"`
|
Position int `json:"position"`
|
||||||
|
DiscNumber int `json:"disc_number"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CoverDownloadResponse represents the response from cover download
|
|
||||||
type CoverDownloadResponse struct {
|
type CoverDownloadResponse struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -37,90 +45,113 @@ type CoverDownloadResponse struct {
|
|||||||
AlreadyExists bool `json:"already_exists,omitempty"`
|
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CoverClient handles cover art downloading
|
type HeaderDownloadRequest struct {
|
||||||
|
HeaderURL string `json:"header_url"`
|
||||||
|
ArtistName string `json:"artist_name"`
|
||||||
|
OutputDir string `json:"output_dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderDownloadResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
File string `json:"file,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type CoverClient struct {
|
type CoverClient struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCoverClient creates a new cover client
|
|
||||||
func NewCoverClient() *CoverClient {
|
func NewCoverClient() *CoverClient {
|
||||||
return &CoverClient{
|
return &CoverClient{
|
||||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildCoverFilename builds the cover filename based on settings (same as track filename)
|
func buildCoverFilename(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat string, includeTrackNumber bool, position, discNumber int) string {
|
||||||
func buildCoverFilename(trackName, artistName, filenameFormat string, includeTrackNumber bool, position int) string {
|
|
||||||
safeTitle := sanitizeFilename(trackName)
|
safeTitle := sanitizeFilename(trackName)
|
||||||
safeArtist := sanitizeFilename(artistName)
|
safeArtist := sanitizeFilename(artistName)
|
||||||
|
safeAlbum := sanitizeFilename(albumName)
|
||||||
|
safeAlbumArtist := sanitizeFilename(albumArtist)
|
||||||
|
|
||||||
|
year := ""
|
||||||
|
if len(releaseDate) >= 4 {
|
||||||
|
year = releaseDate[:4]
|
||||||
|
}
|
||||||
|
|
||||||
var filename string
|
var filename string
|
||||||
|
|
||||||
// Check if format is a template (contains {})
|
|
||||||
if strings.Contains(filenameFormat, "{") {
|
if strings.Contains(filenameFormat, "{") {
|
||||||
filename = filenameFormat
|
filename = filenameFormat
|
||||||
filename = strings.ReplaceAll(filename, "{title}", safeTitle)
|
filename = strings.ReplaceAll(filename, "{title}", safeTitle)
|
||||||
filename = strings.ReplaceAll(filename, "{artist}", safeArtist)
|
filename = strings.ReplaceAll(filename, "{artist}", safeArtist)
|
||||||
|
filename = strings.ReplaceAll(filename, "{album}", safeAlbum)
|
||||||
|
filename = strings.ReplaceAll(filename, "{album_artist}", safeAlbumArtist)
|
||||||
|
filename = strings.ReplaceAll(filename, "{year}", year)
|
||||||
|
filename = strings.ReplaceAll(filename, "{date}", sanitizeFilename(releaseDate))
|
||||||
|
|
||||||
|
if discNumber > 0 {
|
||||||
|
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
|
||||||
|
} else {
|
||||||
|
filename = strings.ReplaceAll(filename, "{disc}", "")
|
||||||
|
}
|
||||||
|
|
||||||
// Handle track number - if position is 0, remove {track} and surrounding separators
|
|
||||||
if position > 0 {
|
if position > 0 {
|
||||||
filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", position))
|
filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", position))
|
||||||
} else {
|
} else {
|
||||||
// Remove {track} with common separators
|
|
||||||
filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "")
|
filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "")
|
||||||
filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "")
|
filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "")
|
||||||
filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "")
|
filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy format support
|
|
||||||
switch filenameFormat {
|
switch filenameFormat {
|
||||||
case "artist-title":
|
case "artist-title":
|
||||||
filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle)
|
filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle)
|
||||||
|
case "title-artist":
|
||||||
|
filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
||||||
case "title":
|
case "title":
|
||||||
filename = safeTitle
|
filename = safeTitle
|
||||||
default: // "title-artist"
|
default:
|
||||||
filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add track number prefix if enabled (legacy behavior)
|
|
||||||
if includeTrackNumber && position > 0 {
|
if includeTrackNumber && position > 0 {
|
||||||
filename = fmt.Sprintf("%02d. %s", position, filename)
|
filename = fmt.Sprintf("%02d - %s", position, filename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filename + ".jpg"
|
return filename + ".jpg"
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMaxResolutionURL converts a Spotify cover URL to max resolution
|
func convertSmallToMedium(imageURL string) string {
|
||||||
// Falls back to original URL if max resolution is not available
|
if strings.Contains(imageURL, spotifySize300) {
|
||||||
func (c *CoverClient) getMaxResolutionURL(coverURL string) string {
|
return strings.Replace(imageURL, spotifySize300, spotifySize640, 1)
|
||||||
// Try to convert to max resolution
|
|
||||||
if strings.Contains(coverURL, spotifySize640) {
|
|
||||||
maxURL := strings.Replace(coverURL, spotifySize640, spotifySizeMax, 1)
|
|
||||||
// Check if max resolution URL is available
|
|
||||||
resp, err := c.httpClient.Head(maxURL)
|
|
||||||
if err == nil && resp.StatusCode == http.StatusOK {
|
|
||||||
return maxURL
|
|
||||||
}
|
}
|
||||||
}
|
return imageURL
|
||||||
// Return original URL as fallback
|
}
|
||||||
return coverURL
|
|
||||||
|
func (c *CoverClient) getMaxResolutionURL(imageURL string) string {
|
||||||
|
|
||||||
|
mediumURL := convertSmallToMedium(imageURL)
|
||||||
|
if strings.Contains(mediumURL, spotifySize640) {
|
||||||
|
return strings.Replace(mediumURL, spotifySize640, spotifySizeMax, 1)
|
||||||
|
}
|
||||||
|
return mediumURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadCoverToPath downloads cover art from URL to a specific path
|
|
||||||
// If embedMaxQualityCover is true, it will try to get max resolution
|
|
||||||
func (c *CoverClient) DownloadCoverToPath(coverURL, outputPath string, embedMaxQualityCover bool) error {
|
func (c *CoverClient) DownloadCoverToPath(coverURL, outputPath string, embedMaxQualityCover bool) error {
|
||||||
if coverURL == "" {
|
if coverURL == "" {
|
||||||
return fmt.Errorf("cover URL is required")
|
return fmt.Errorf("cover URL is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use max quality URL if setting is enabled
|
downloadURL := convertSmallToMedium(coverURL)
|
||||||
downloadURL := coverURL
|
|
||||||
if embedMaxQualityCover {
|
if embedMaxQualityCover {
|
||||||
downloadURL = c.getMaxResolutionURL(coverURL)
|
downloadURL = c.getMaxResolutionURL(downloadURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download cover image
|
|
||||||
resp, err := c.httpClient.Get(downloadURL)
|
resp, err := c.httpClient.Get(downloadURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to download cover: %v", err)
|
return fmt.Errorf("failed to download cover: %v", err)
|
||||||
@@ -131,14 +162,12 @@ func (c *CoverClient) DownloadCoverToPath(coverURL, outputPath string, embedMaxQ
|
|||||||
return fmt.Errorf("failed to download cover: HTTP %d", resp.StatusCode)
|
return fmt.Errorf("failed to download cover: HTTP %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create file
|
|
||||||
file, err := os.Create(outputPath)
|
file, err := os.Create(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create file: %v", err)
|
return fmt.Errorf("failed to create file: %v", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// Write content to file
|
|
||||||
_, err = io.Copy(file, resp.Body)
|
_, err = io.Copy(file, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write cover file: %v", err)
|
return fmt.Errorf("failed to write cover file: %v", err)
|
||||||
@@ -147,7 +176,69 @@ func (c *CoverClient) DownloadCoverToPath(coverURL, outputPath string, embedMaxQ
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadCover downloads cover art for a single track
|
func (c *CoverClient) ApplyMacOSFLACFileIcon(filePath, coverURL string, iconSize int, embedMaxQualityCover bool) error {
|
||||||
|
if filePath == "" {
|
||||||
|
return fmt.Errorf("file path is required")
|
||||||
|
}
|
||||||
|
if coverURL == "" {
|
||||||
|
return fmt.Errorf("cover URL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile, err := os.CreateTemp("", "spotiflac-file-icon-*.jpg")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temporary cover file: %w", err)
|
||||||
|
}
|
||||||
|
tmpPath := tmpFile.Name()
|
||||||
|
tmpFile.Close()
|
||||||
|
defer os.Remove(tmpPath)
|
||||||
|
|
||||||
|
if err := c.DownloadCoverToPath(coverURL, tmpPath, embedMaxQualityCover); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetMacOSFileIconFromImage(filePath, tmpPath, iconSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResizeImageForIcon(sourcePath string, iconSize int) (string, error) {
|
||||||
|
if sourcePath == "" {
|
||||||
|
return "", fmt.Errorf("source image path is required")
|
||||||
|
}
|
||||||
|
if iconSize <= 0 {
|
||||||
|
iconSize = 256
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to open source image: %w", err)
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
srcImage, _, err := image.Decode(in)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode source image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := image.NewRGBA(image.Rect(0, 0, iconSize, iconSize))
|
||||||
|
xdraw.CatmullRom.Scale(dst, dst.Bounds(), srcImage, srcImage.Bounds(), xdraw.Over, nil)
|
||||||
|
|
||||||
|
tmpFile, err := os.CreateTemp("", "spotiflac-resized-icon-*.png")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create resized icon temp file: %w", err)
|
||||||
|
}
|
||||||
|
tmpPath := tmpFile.Name()
|
||||||
|
defer tmpFile.Close()
|
||||||
|
|
||||||
|
var encoded bytes.Buffer
|
||||||
|
if err := png.Encode(&encoded, dst); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to encode resized icon image: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(tmpFile, &encoded); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write resized icon image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadResponse, error) {
|
func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadResponse, error) {
|
||||||
if req.CoverURL == "" {
|
if req.CoverURL == "" {
|
||||||
return &CoverDownloadResponse{
|
return &CoverDownloadResponse{
|
||||||
@@ -156,7 +247,6 @@ func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadRes
|
|||||||
}, fmt.Errorf("cover URL is required")
|
}, fmt.Errorf("cover URL is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create output directory if it doesn't exist
|
|
||||||
outputDir := req.OutputDir
|
outputDir := req.OutputDir
|
||||||
if outputDir == "" {
|
if outputDir == "" {
|
||||||
outputDir = GetDefaultMusicPath()
|
outputDir = GetDefaultMusicPath()
|
||||||
@@ -171,15 +261,13 @@ func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadRes
|
|||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate filename using same format as track
|
|
||||||
filenameFormat := req.FilenameFormat
|
filenameFormat := req.FilenameFormat
|
||||||
if filenameFormat == "" {
|
if filenameFormat == "" {
|
||||||
filenameFormat = "title-artist" // default
|
filenameFormat = "title-artist"
|
||||||
}
|
}
|
||||||
filename := buildCoverFilename(req.TrackName, req.ArtistName, filenameFormat, req.TrackNumber, req.Position)
|
filename := buildCoverFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, filenameFormat, req.TrackNumber, req.Position, req.DiscNumber)
|
||||||
filePath := filepath.Join(outputDir, filename)
|
filePath := filepath.Join(outputDir, filename)
|
||||||
|
|
||||||
// Check if file already exists
|
|
||||||
if fileInfo, err := os.Stat(filePath); err == nil && fileInfo.Size() > 0 {
|
if fileInfo, err := os.Stat(filePath); err == nil && fileInfo.Size() > 0 {
|
||||||
return &CoverDownloadResponse{
|
return &CoverDownloadResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -189,10 +277,8 @@ func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadRes
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get max resolution URL, fallback to original
|
|
||||||
downloadURL := c.getMaxResolutionURL(req.CoverURL)
|
downloadURL := c.getMaxResolutionURL(req.CoverURL)
|
||||||
|
|
||||||
// Download cover image
|
|
||||||
resp, err := c.httpClient.Get(downloadURL)
|
resp, err := c.httpClient.Get(downloadURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &CoverDownloadResponse{
|
return &CoverDownloadResponse{
|
||||||
@@ -209,7 +295,6 @@ func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadRes
|
|||||||
}, fmt.Errorf("HTTP %d", resp.StatusCode)
|
}, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create file
|
|
||||||
file, err := os.Create(filePath)
|
file, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &CoverDownloadResponse{
|
return &CoverDownloadResponse{
|
||||||
@@ -219,7 +304,6 @@ func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadRes
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// Write content to file
|
|
||||||
_, err = io.Copy(file, resp.Body)
|
_, err = io.Copy(file, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &CoverDownloadResponse{
|
return &CoverDownloadResponse{
|
||||||
@@ -234,3 +318,278 @@ func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadRes
|
|||||||
File: filePath,
|
File: filePath,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CoverClient) DownloadHeader(req HeaderDownloadRequest) (*HeaderDownloadResponse, error) {
|
||||||
|
if req.HeaderURL == "" {
|
||||||
|
return &HeaderDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Header URL is required",
|
||||||
|
}, fmt.Errorf("header URL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ArtistName == "" {
|
||||||
|
return &HeaderDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Artist name is required",
|
||||||
|
}, fmt.Errorf("artist name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputDir := req.OutputDir
|
||||||
|
if outputDir == "" {
|
||||||
|
outputDir = GetDefaultMusicPath()
|
||||||
|
} else {
|
||||||
|
outputDir = NormalizePath(outputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
artistFolder := filepath.Join(outputDir, sanitizeFilename(req.ArtistName))
|
||||||
|
if err := os.MkdirAll(artistFolder, 0755); err != nil {
|
||||||
|
return &HeaderDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to create artist folder: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := sanitizeFilename(req.ArtistName) + "_Header.jpg"
|
||||||
|
filePath := filepath.Join(artistFolder, filename)
|
||||||
|
|
||||||
|
if fileInfo, err := os.Stat(filePath); err == nil && fileInfo.Size() > 0 {
|
||||||
|
return &HeaderDownloadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Header file already exists",
|
||||||
|
File: filePath,
|
||||||
|
AlreadyExists: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Get(req.HeaderURL)
|
||||||
|
if err != nil {
|
||||||
|
return &HeaderDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to download header: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return &HeaderDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to download header: HTTP %d", resp.StatusCode),
|
||||||
|
}, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return &HeaderDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to create file: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return &HeaderDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to write header file: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HeaderDownloadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Header downloaded successfully",
|
||||||
|
File: filePath,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GalleryImageDownloadRequest struct {
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ArtistName string `json:"artist_name"`
|
||||||
|
ImageIndex int `json:"image_index"`
|
||||||
|
OutputDir string `json:"output_dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GalleryImageDownloadResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
File string `json:"file,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CoverClient) DownloadGalleryImage(req GalleryImageDownloadRequest) (*GalleryImageDownloadResponse, error) {
|
||||||
|
if req.ImageURL == "" {
|
||||||
|
return &GalleryImageDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Image URL is required",
|
||||||
|
}, fmt.Errorf("image URL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ArtistName == "" {
|
||||||
|
return &GalleryImageDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Artist name is required",
|
||||||
|
}, fmt.Errorf("artist name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputDir := req.OutputDir
|
||||||
|
if outputDir == "" {
|
||||||
|
outputDir = GetDefaultMusicPath()
|
||||||
|
} else {
|
||||||
|
outputDir = NormalizePath(outputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
artistFolder := filepath.Join(outputDir, sanitizeFilename(req.ArtistName))
|
||||||
|
if err := os.MkdirAll(artistFolder, 0755); err != nil {
|
||||||
|
return &GalleryImageDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to create artist folder: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := sanitizeFilename(req.ArtistName) + fmt.Sprintf("_Gallery_%d.jpg", req.ImageIndex+1)
|
||||||
|
filePath := filepath.Join(artistFolder, filename)
|
||||||
|
|
||||||
|
if fileInfo, err := os.Stat(filePath); err == nil && fileInfo.Size() > 0 {
|
||||||
|
return &GalleryImageDownloadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Gallery image file already exists",
|
||||||
|
File: filePath,
|
||||||
|
AlreadyExists: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Get(req.ImageURL)
|
||||||
|
if err != nil {
|
||||||
|
return &GalleryImageDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to download gallery image: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return &GalleryImageDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to download gallery image: HTTP %d", resp.StatusCode),
|
||||||
|
}, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return &GalleryImageDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to create file: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return &GalleryImageDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to write gallery image file: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GalleryImageDownloadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Gallery image downloaded successfully",
|
||||||
|
File: filePath,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AvatarDownloadRequest struct {
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
ArtistName string `json:"artist_name"`
|
||||||
|
OutputDir string `json:"output_dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AvatarDownloadResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
File string `json:"file,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CoverClient) DownloadAvatar(req AvatarDownloadRequest) (*AvatarDownloadResponse, error) {
|
||||||
|
if req.AvatarURL == "" {
|
||||||
|
return &AvatarDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Avatar URL is required",
|
||||||
|
}, fmt.Errorf("avatar URL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ArtistName == "" {
|
||||||
|
return &AvatarDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: "Artist name is required",
|
||||||
|
}, fmt.Errorf("artist name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputDir := req.OutputDir
|
||||||
|
if outputDir == "" {
|
||||||
|
outputDir = GetDefaultMusicPath()
|
||||||
|
} else {
|
||||||
|
outputDir = NormalizePath(outputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
artistFolder := filepath.Join(outputDir, sanitizeFilename(req.ArtistName))
|
||||||
|
if err := os.MkdirAll(artistFolder, 0755); err != nil {
|
||||||
|
return &AvatarDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to create artist folder: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := sanitizeFilename(req.ArtistName) + "_Avatar.jpg"
|
||||||
|
filePath := filepath.Join(artistFolder, filename)
|
||||||
|
|
||||||
|
if fileInfo, err := os.Stat(filePath); err == nil && fileInfo.Size() > 0 {
|
||||||
|
return &AvatarDownloadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Avatar file already exists",
|
||||||
|
File: filePath,
|
||||||
|
AlreadyExists: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Get(req.AvatarURL)
|
||||||
|
if err != nil {
|
||||||
|
return &AvatarDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to download avatar: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return &AvatarDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to download avatar: HTTP %d", resp.StatusCode),
|
||||||
|
}, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return &AvatarDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to create file: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return &AvatarDownloadResponse{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to write avatar file: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AvatarDownloadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "Avatar downloaded successfully",
|
||||||
|
File: filePath,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrDownloadCancelled = errors.New("download cancelled")
|
||||||
|
|
||||||
|
var downloadCancelState = struct {
|
||||||
|
sync.Mutex
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
active int
|
||||||
|
stopping bool
|
||||||
|
}{}
|
||||||
|
|
||||||
|
func BeginDownloadCancellationScope() (context.Context, func()) {
|
||||||
|
downloadCancelState.Lock()
|
||||||
|
defer downloadCancelState.Unlock()
|
||||||
|
|
||||||
|
if downloadCancelState.ctx == nil || downloadCancelState.active == 0 {
|
||||||
|
downloadCancelState.ctx, downloadCancelState.cancel = context.WithCancel(context.Background())
|
||||||
|
downloadCancelState.stopping = false
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadCancelState.active++
|
||||||
|
ctx := downloadCancelState.ctx
|
||||||
|
once := sync.Once{}
|
||||||
|
|
||||||
|
return ctx, func() {
|
||||||
|
once.Do(func() {
|
||||||
|
downloadCancelState.Lock()
|
||||||
|
defer downloadCancelState.Unlock()
|
||||||
|
|
||||||
|
if downloadCancelState.active > 0 {
|
||||||
|
downloadCancelState.active--
|
||||||
|
}
|
||||||
|
if downloadCancelState.active == 0 {
|
||||||
|
if downloadCancelState.cancel != nil {
|
||||||
|
downloadCancelState.cancel()
|
||||||
|
}
|
||||||
|
downloadCancelState.ctx = nil
|
||||||
|
downloadCancelState.cancel = nil
|
||||||
|
downloadCancelState.stopping = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ActiveDownloadContext() context.Context {
|
||||||
|
downloadCancelState.Lock()
|
||||||
|
defer downloadCancelState.Unlock()
|
||||||
|
|
||||||
|
if downloadCancelState.ctx == nil {
|
||||||
|
return context.Background()
|
||||||
|
}
|
||||||
|
return downloadCancelState.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForceStopActiveDownloads() {
|
||||||
|
downloadCancelState.Lock()
|
||||||
|
cancel := downloadCancelState.cancel
|
||||||
|
if cancel != nil {
|
||||||
|
downloadCancelState.stopping = true
|
||||||
|
}
|
||||||
|
downloadCancelState.Unlock()
|
||||||
|
|
||||||
|
if cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelQueuedAndDownloadingItems()
|
||||||
|
SetDownloading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsDownloadForceStopRequested() bool {
|
||||||
|
downloadCancelState.Lock()
|
||||||
|
defer downloadCancelState.Unlock()
|
||||||
|
|
||||||
|
return downloadCancelState.stopping
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckDownloadCancelled() error {
|
||||||
|
ctx := ActiveDownloadContext()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrDownloadCancelled
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SleepWithDownloadContext(delay time.Duration) error {
|
||||||
|
if delay <= 0 {
|
||||||
|
return CheckDownloadCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := ActiveDownloadContext()
|
||||||
|
timer := time.NewTimer(delay)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrDownloadCancelled
|
||||||
|
case <-timer.C:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsDownloadCancelledError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return errors.Is(err, ErrDownloadCancelled) || errors.Is(err, context.Canceled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapDownloadCancelled(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if IsDownloadForceStopRequested() || errors.Is(err, context.Canceled) {
|
||||||
|
return fmt.Errorf("%w", ErrDownloadCancelled)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
previewMaxSeconds = 35
|
||||||
|
previewExpectedMinSeconds = 60
|
||||||
|
largeMismatchMinExpected = 90
|
||||||
|
minAllowedDurationDiff = 15
|
||||||
|
durationDiffRatio = 0.25
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateDownloadedTrackDuration(filePath string, expectedSeconds int) (bool, error) {
|
||||||
|
if filePath == "" || expectedSeconds <= 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actualDuration, err := GetAudioDuration(filePath)
|
||||||
|
if err != nil || actualDuration <= 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actualSeconds := int(math.Round(actualDuration))
|
||||||
|
if actualSeconds <= 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedSeconds >= previewExpectedMinSeconds && actualSeconds <= previewMaxSeconds {
|
||||||
|
return true, fmt.Errorf("detected preview/sample download: file is %ds, expected about %ds. file was removed", actualSeconds, expectedSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedSeconds >= largeMismatchMinExpected {
|
||||||
|
allowedDiff := int(math.Max(minAllowedDurationDiff, math.Round(float64(expectedSeconds)*durationDiffRatio)))
|
||||||
|
diff := int(math.Abs(float64(actualSeconds - expectedSeconds)))
|
||||||
|
if diff > allowedDiff {
|
||||||
|
return true, fmt.Errorf("downloaded file duration mismatch: file is %ds, expected about %ds. file was removed", actualSeconds, expectedSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package backend
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,30 +11,60 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ulikunitz/xz"
|
"github.com/ulikunitz/xz"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// decodeBase64 decodes a base64 encoded string
|
type executableCandidate struct {
|
||||||
func decodeBase64(encoded string) (string, error) {
|
path string
|
||||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
source string
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(decoded), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func ValidateExecutable(path string) error {
|
||||||
ffmpegWindowsURL = "aHR0cHM6Ly9naXRodWIuY29tL0J0Yk4vRkZtcGVnLUJ1aWxkcy9yZWxlYXNlcy9kb3dubG9hZC9sYXRlc3QvZmZtcGVnLW1hc3Rlci1sYXRlc3Qtd2luNjQtZ3BsLnppcA=="
|
cleanedPath := filepath.Clean(path)
|
||||||
ffmpegLinuxURL = "aHR0cHM6Ly9naXRodWIuY29tL0J0Yk4vRkZtcGVnLUJ1aWxkcy9yZWxlYXNlcy9kb3dubG9hZC9sYXRlc3QvZmZtcGVnLW1hc3Rlci1sYXRlc3QtbGludXg2NC1ncGwudGFyLnh6"
|
if cleanedPath == "" {
|
||||||
ffmpegMacOSURL = "aHR0cHM6Ly9ldmVybWVldC5jeC9mZm1wZWcvZ2V0cmVsZWFzZS96aXA="
|
return fmt.Errorf("empty path")
|
||||||
ffprobeMacOSURL = "aHR0cHM6Ly9ldmVybWVldC5jeC9mZm1wZWcvZ2V0cmVsZWFzZS9mZnByb2JlL3ppcA=="
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// GetFFmpegDir returns the directory where ffmpeg should be stored
|
if !filepath.IsAbs(cleanedPath) {
|
||||||
func GetFFmpegDir() (string, error) {
|
return fmt.Errorf("path must be absolute: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(cleanedPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return fmt.Errorf("path is a directory: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if info.Mode()&0111 == 0 {
|
||||||
|
return fmt.Errorf("file is not executable: %s", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base := filepath.Base(cleanedPath)
|
||||||
|
validNames := map[string]bool{
|
||||||
|
"ffmpeg": true,
|
||||||
|
"ffmpeg.exe": true,
|
||||||
|
"ffprobe": true,
|
||||||
|
"ffprobe.exe": true,
|
||||||
|
}
|
||||||
|
if !validNames[base] {
|
||||||
|
return fmt.Errorf("invalid executable name: %s", base)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAppDir() (string, error) {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get home directory: %w", err)
|
return "", fmt.Errorf("failed to get home directory: %w", err)
|
||||||
@@ -42,155 +72,402 @@ func GetFFmpegDir() (string, error) {
|
|||||||
return filepath.Join(homeDir, ".spotiflac"), nil
|
return filepath.Join(homeDir, ".spotiflac"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFFmpegPath returns the full path to the ffmpeg executable
|
func EnsureAppDir() (string, error) {
|
||||||
func GetFFmpegPath() (string, error) {
|
appDir, err := GetAppDir()
|
||||||
ffmpegDir, err := GetFFmpegDir()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(appDir, 0o755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create app directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return appDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFFmpegDir() (string, error) {
|
||||||
|
return EnsureAppDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyExecutable(src, dst string) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
out, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(out, in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := out.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepareExecutableForUse(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendExecutableCandidate(candidates []executableCandidate, seen map[string]struct{}, path, source string) []executableCandidate {
|
||||||
|
cleanedPath := filepath.Clean(strings.TrimSpace(path))
|
||||||
|
if cleanedPath == "" {
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
if _, exists := seen[cleanedPath]; exists {
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[cleanedPath] = struct{}{}
|
||||||
|
return append(candidates, executableCandidate{
|
||||||
|
path: cleanedPath,
|
||||||
|
source: source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSystemExecutable(executableName string) string {
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
candidates := []string{
|
||||||
|
"/opt/homebrew/bin/" + executableName,
|
||||||
|
"/usr/local/bin/" + executableName,
|
||||||
|
}
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
path, err := exec.Command("which", executableName).Output()
|
||||||
|
if err == nil {
|
||||||
|
trimmed := strings.TrimSpace(string(path))
|
||||||
|
if trimmed != "" {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := exec.LookPath(executableName)
|
||||||
|
if err == nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func runExecutableVersionCheck(path string) error {
|
||||||
|
cmd := exec.Command(path, "-version")
|
||||||
|
setHideWindow(cmd)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeMacOSQuarantineAttribute(path string) error {
|
||||||
|
cmd := exec.Command("xattr", "-d", "com.apple.quarantine", path)
|
||||||
|
setHideWindow(cmd)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedOutput := strings.TrimSpace(string(output))
|
||||||
|
lowerOutput := strings.ToLower(trimmedOutput)
|
||||||
|
if strings.Contains(lowerOutput, "no such xattr") || strings.Contains(lowerOutput, "attribute not found") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimmedOutput != "" {
|
||||||
|
return fmt.Errorf("%w: %s", err, trimmedOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareExecutableForUse(path string) error {
|
||||||
|
cleanedPath := filepath.Clean(strings.TrimSpace(path))
|
||||||
|
if cleanedPath == "" {
|
||||||
|
return fmt.Errorf("empty path")
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chmod(cleanedPath, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to mark executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
if err := removeMacOSQuarantineAttribute(cleanedPath); err != nil {
|
||||||
|
fmt.Printf("[FFmpeg] Warning: failed to remove macOS quarantine from %s: %v\n", cleanedPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveExecutablePath(executableName string) (string, string, error) {
|
||||||
|
ffmpegDir, err := GetFFmpegDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
localPath := filepath.Join(ffmpegDir, executableName)
|
||||||
|
nextDir := filepath.Join(filepath.Dir(ffmpegDir), ".spotiflac-next")
|
||||||
|
nextPath := filepath.Join(nextDir, executableName)
|
||||||
|
localExists := false
|
||||||
|
candidates := make([]executableCandidate, 0, 3)
|
||||||
|
seen := make(map[string]struct{}, 3)
|
||||||
|
|
||||||
|
if systemPath := resolveSystemExecutable(executableName); systemPath != "" {
|
||||||
|
candidates = appendExecutableCandidate(candidates, seen, systemPath, "system")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(localPath); err == nil {
|
||||||
|
localExists = true
|
||||||
|
candidates = appendExecutableCandidate(candidates, seen, localPath, "local")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !localExists {
|
||||||
|
if _, err := os.Stat(nextPath); err == nil {
|
||||||
|
if copyErr := copyExecutable(nextPath, localPath); copyErr == nil {
|
||||||
|
fmt.Printf("[FFmpeg] Copied %s from SpotiFLAC-Next folder\n", executableName)
|
||||||
|
candidates = appendExecutableCandidate(candidates, seen, localPath, "migrated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if candidate.source != "system" {
|
||||||
|
if err := prepareExecutableForUse(candidate.path); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
fmt.Printf("[FFmpeg] Skipping %s %s: %v\n", candidate.source, candidate.path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateExecutable(candidate.path); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
fmt.Printf("[FFmpeg] Skipping %s %s: %v\n", candidate.source, candidate.path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runExecutableVersionCheck(candidate.path); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
fmt.Printf("[FFmpeg] Skipping %s %s: %v\n", candidate.source, candidate.path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate.path, localPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidates) > 0 {
|
||||||
|
if lastErr != nil {
|
||||||
|
return "", localPath, fmt.Errorf("no working %s executable found: %w", executableName, lastErr)
|
||||||
|
}
|
||||||
|
return "", localPath, fmt.Errorf("no working %s executable found", executableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", localPath, fmt.Errorf("%s not found in app directory or system path", executableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFFmpegPath() (string, error) {
|
||||||
ffmpegName := "ffmpeg"
|
ffmpegName := "ffmpeg"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
ffmpegName = "ffmpeg.exe"
|
ffmpegName = "ffmpeg.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(ffmpegDir, ffmpegName), nil
|
path, localPath, err := resolveExecutablePath(ffmpegName)
|
||||||
}
|
|
||||||
|
|
||||||
// GetFFprobePath returns the full path to the ffprobe executable in app directory
|
|
||||||
func GetFFprobePath() (string, error) {
|
|
||||||
ffmpegDir, err := GetFFmpegDir()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if localPath != "" {
|
||||||
|
return localPath, err
|
||||||
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFFprobePath() (string, error) {
|
||||||
ffprobeName := "ffprobe"
|
ffprobeName := "ffprobe"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
ffprobeName = "ffprobe.exe"
|
ffprobeName = "ffprobe.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
ffprobePath := filepath.Join(ffmpegDir, ffprobeName)
|
path, localPath, err := resolveExecutablePath(ffprobeName)
|
||||||
if _, err := os.Stat(ffprobePath); err == nil {
|
if err != nil {
|
||||||
return ffprobePath, nil
|
if localPath != "" {
|
||||||
|
return localPath, err
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("ffprobe not found in app directory")
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFFprobeInstalled checks if ffprobe is installed in the app directory
|
|
||||||
func IsFFprobeInstalled() (bool, error) {
|
func IsFFprobeInstalled() (bool, error) {
|
||||||
ffprobePath, err := GetFFprobePath()
|
_, err := GetFFprobePath()
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify it's executable
|
|
||||||
cmd := exec.Command(ffprobePath, "-version")
|
|
||||||
setHideWindow(cmd)
|
|
||||||
err = cmd.Run()
|
|
||||||
return err == nil, nil
|
return err == nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFFmpegInstalled checks if ffmpeg is installed in the app directory
|
|
||||||
func IsFFmpegInstalled() (bool, error) {
|
func IsFFmpegInstalled() (bool, error) {
|
||||||
ffmpegPath, err := GetFFmpegPath()
|
if _, err := GetFFmpegPath(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = os.Stat(ffmpegPath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return false, err
|
return IsFFprobeInstalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBrewPath() string {
|
||||||
|
brewPaths := []string{
|
||||||
|
"/opt/homebrew/bin/brew",
|
||||||
|
"/usr/local/bin/brew",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify it's executable
|
for _, path := range brewPaths {
|
||||||
cmd := exec.Command(ffmpegPath, "-version")
|
if _, err := os.Stat(path); err == nil {
|
||||||
// Hide console window on Windows
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsBrewFFmpegInstalled() (bool, error) {
|
||||||
|
brewPath := GetBrewPath()
|
||||||
|
if brewPath == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(brewPath, "list", "ffmpeg")
|
||||||
setHideWindow(cmd)
|
setHideWindow(cmd)
|
||||||
err = cmd.Run()
|
err := cmd.Run()
|
||||||
return err == nil, nil
|
return err == nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadFFmpeg downloads and extracts ffmpeg to the app directory
|
func InstallFFmpegWithBrew(progressCallback func(int, string)) error {
|
||||||
|
brewPath := GetBrewPath()
|
||||||
|
if brewPath == "" {
|
||||||
|
return fmt.Errorf("brew not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback(10, "Installing FFmpeg via Homebrew...")
|
||||||
|
|
||||||
|
cmd := exec.Command(brewPath, "install", "ffmpeg")
|
||||||
|
setHideWindow(cmd)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to install ffmpeg: %w - %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback(100, "done")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ffmpegReleaseBaseURL = "https://github.com/spotbye/Dependencies/releases/download/FFmpeg-8.1"
|
||||||
|
|
||||||
|
func buildFFmpegReleaseURL(assetName string) string {
|
||||||
|
return ffmpegReleaseBaseURL + "/" + assetName
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFFmpegDownloadURLs() ([]string, []string, error) {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return []string{buildFFmpegReleaseURL("ffmpeg-windows.zip")}, []string{buildFFmpegReleaseURL("ffprobe-windows.zip")}, nil
|
||||||
|
case "linux":
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "amd64":
|
||||||
|
return []string{buildFFmpegReleaseURL("ffmpeg-linux-amd64.zip")}, []string{buildFFmpegReleaseURL("ffprobe-linux-amd64.zip")}, nil
|
||||||
|
case "arm64":
|
||||||
|
return []string{buildFFmpegReleaseURL("ffmpeg-linux-arm64v8.zip")}, []string{buildFFmpegReleaseURL("ffprobe-linux-arm64v8.zip")}, nil
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unsupported Linux architecture: %s", runtime.GOARCH)
|
||||||
|
}
|
||||||
|
case "darwin":
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "amd64":
|
||||||
|
return []string{buildFFmpegReleaseURL("ffmpeg-macos-amd64.zip")}, []string{buildFFmpegReleaseURL("ffprobe-macos-amd64.zip")}, nil
|
||||||
|
case "arm64":
|
||||||
|
return []string{buildFFmpegReleaseURL("ffmpeg-macos-arm64.zip")}, []string{buildFFmpegReleaseURL("ffprobe-macos-arm64.zip")}, nil
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unsupported macOS architecture: %s", runtime.GOARCH)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func DownloadFFmpeg(progressCallback func(int)) error {
|
func DownloadFFmpeg(progressCallback func(int)) error {
|
||||||
|
|
||||||
|
SetDownloadProgress(0)
|
||||||
|
SetDownloadSpeed(0)
|
||||||
|
SetDownloading(true)
|
||||||
|
defer SetDownloading(false)
|
||||||
|
|
||||||
ffmpegDir, err := GetFFmpegDir()
|
ffmpegDir, err := GetFFmpegDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
|
||||||
if err := os.MkdirAll(ffmpegDir, 0755); err != nil {
|
if err := os.MkdirAll(ffmpegDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create ffmpeg directory: %w", err)
|
return fmt.Errorf("failed to create ffmpeg directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For macOS, download ffmpeg and ffprobe separately (only if not already installed)
|
|
||||||
if runtime.GOOS == "darwin" {
|
|
||||||
ffmpegInstalled, _ := IsFFmpegInstalled()
|
ffmpegInstalled, _ := IsFFmpegInstalled()
|
||||||
ffprobeInstalled, _ := IsFFprobeInstalled()
|
ffprobeInstalled, _ := IsFFprobeInstalled()
|
||||||
|
|
||||||
if !ffmpegInstalled && !ffprobeInstalled {
|
ffmpegURLs, ffprobeURLs, err := getFFmpegDownloadURLs()
|
||||||
// Download both
|
if err != nil {
|
||||||
ffmpegURL, _ := decodeBase64(ffmpegMacOSURL)
|
|
||||||
fmt.Printf("[FFmpeg] Downloading ffmpeg from: %s\n", ffmpegURL)
|
|
||||||
if err := downloadAndExtract(ffmpegURL, ffmpegDir, progressCallback, 0, 50); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ffprobeURL, _ := decodeBase64(ffprobeMacOSURL)
|
if !ffmpegInstalled && !ffprobeInstalled {
|
||||||
fmt.Printf("[FFmpeg] Downloading ffprobe from: %s\n", ffprobeURL)
|
if err := downloadWithFallback(ffmpegURLs, ffmpegDir, progressCallback, 0, 50); err != nil {
|
||||||
if err := downloadAndExtract(ffprobeURL, ffmpegDir, progressCallback, 50, 100); err != nil {
|
|
||||||
return fmt.Errorf("failed to download ffprobe: %w", err)
|
|
||||||
}
|
|
||||||
} else if !ffmpegInstalled {
|
|
||||||
// Only download ffmpeg
|
|
||||||
ffmpegURL, _ := decodeBase64(ffmpegMacOSURL)
|
|
||||||
fmt.Printf("[FFmpeg] Downloading ffmpeg from: %s\n", ffmpegURL)
|
|
||||||
if err := downloadAndExtract(ffmpegURL, ffmpegDir, progressCallback, 0, 100); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if !ffprobeInstalled {
|
if err := downloadWithFallback(ffprobeURLs, ffmpegDir, progressCallback, 50, 100); err != nil {
|
||||||
// Only download ffprobe
|
return err
|
||||||
ffprobeURL, _ := decodeBase64(ffprobeMacOSURL)
|
|
||||||
fmt.Printf("[FFmpeg] Downloading ffprobe from: %s\n", ffprobeURL)
|
|
||||||
if err := downloadAndExtract(ffprobeURL, ffmpegDir, progressCallback, 0, 100); err != nil {
|
|
||||||
return fmt.Errorf("failed to download ffprobe: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Windows/Linux: single archive contains both ffmpeg and ffprobe
|
if !ffmpegInstalled {
|
||||||
var encodedURL string
|
return downloadWithFallback(ffmpegURLs, ffmpegDir, progressCallback, 0, 100)
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
encodedURL = ffmpegWindowsURL
|
|
||||||
case "linux":
|
|
||||||
encodedURL = ffmpegLinuxURL
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode URL
|
if !ffprobeInstalled {
|
||||||
url, err := decodeBase64(encodedURL)
|
return downloadWithFallback(ffprobeURLs, ffmpegDir, progressCallback, 0, 100)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to decode ffmpeg URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("[FFmpeg] Downloading from: %s\n", url)
|
|
||||||
|
|
||||||
if err := downloadAndExtract(url, ffmpegDir, progressCallback, 0, 100); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadAndExtract downloads a file and extracts it
|
func downloadWithFallback(urls []string, destDir string, progressCallback func(int), start, end int) error {
|
||||||
|
var lastErr error
|
||||||
|
for _, url := range urls {
|
||||||
|
fmt.Printf("[FFmpeg] Trying to download from: %s\n", url)
|
||||||
|
err := downloadAndExtract(url, destDir, progressCallback, start, end)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
fmt.Printf("[FFmpeg] Attempt failed: %v\n", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("all download attempts failed: %w", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
func downloadAndExtract(url, destDir string, progressCallback func(int), progressStart, progressEnd int) error {
|
func downloadAndExtract(url, destDir string, progressCallback func(int), progressStart, progressEnd int) error {
|
||||||
// Create temporary file for download
|
|
||||||
tmpFile, err := os.CreateTemp("", "ffmpeg-*")
|
tmpFile, err := os.CreateTemp("", "ffmpeg-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create temp file: %w", err)
|
return fmt.Errorf("failed to create temp file: %w", err)
|
||||||
@@ -198,8 +475,14 @@ func downloadAndExtract(url, destDir string, progressCallback func(int), progres
|
|||||||
defer os.Remove(tmpFile.Name())
|
defer os.Remove(tmpFile.Name())
|
||||||
defer tmpFile.Close()
|
defer tmpFile.Close()
|
||||||
|
|
||||||
// Download the file
|
client := &http.Client{}
|
||||||
resp, err := http.Get(url)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to download: %w", err)
|
return fmt.Errorf("failed to download: %w", err)
|
||||||
}
|
}
|
||||||
@@ -211,8 +494,16 @@ func downloadAndExtract(url, destDir string, progressCallback func(int), progres
|
|||||||
|
|
||||||
totalSize := resp.ContentLength
|
totalSize := resp.ContentLength
|
||||||
var downloaded int64
|
var downloaded int64
|
||||||
|
lastTime := time.Now()
|
||||||
|
var lastBytes int64
|
||||||
|
|
||||||
|
if totalSize > 0 {
|
||||||
|
totalSizeMB := float64(totalSize) / (1024 * 1024)
|
||||||
|
fmt.Printf("[FFmpeg] Total size: %.2f MB\n", totalSizeMB)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[FFmpeg] Downloading... (size unknown)\n")
|
||||||
|
}
|
||||||
|
|
||||||
// Create a progress reader
|
|
||||||
buf := make([]byte, 32*1024)
|
buf := make([]byte, 32*1024)
|
||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := resp.Body.Read(buf)
|
||||||
@@ -222,12 +513,46 @@ func downloadAndExtract(url, destDir string, progressCallback func(int), progres
|
|||||||
return fmt.Errorf("failed to write to temp file: %w", writeErr)
|
return fmt.Errorf("failed to write to temp file: %w", writeErr)
|
||||||
}
|
}
|
||||||
downloaded += int64(n)
|
downloaded += int64(n)
|
||||||
|
|
||||||
|
mbDownloaded := float64(downloaded) / (1024 * 1024)
|
||||||
|
now := time.Now()
|
||||||
|
timeDiff := now.Sub(lastTime).Seconds()
|
||||||
|
var speedMBps float64
|
||||||
|
|
||||||
|
if timeDiff > 0.1 {
|
||||||
|
bytesDiff := float64(downloaded - lastBytes)
|
||||||
|
speedMBps = (bytesDiff / (1024 * 1024)) / timeDiff
|
||||||
|
lastTime = now
|
||||||
|
lastBytes = downloaded
|
||||||
|
}
|
||||||
|
|
||||||
|
SetDownloadProgress(mbDownloaded)
|
||||||
|
if speedMBps > 0 {
|
||||||
|
SetDownloadSpeed(speedMBps)
|
||||||
|
}
|
||||||
|
|
||||||
if totalSize > 0 && progressCallback != nil {
|
if totalSize > 0 && progressCallback != nil {
|
||||||
// Scale progress between progressStart and progressEnd
|
|
||||||
rawProgress := float64(downloaded) / float64(totalSize)
|
rawProgress := float64(downloaded) / float64(totalSize)
|
||||||
scaledProgress := progressStart + int(rawProgress*float64(progressEnd-progressStart))
|
scaledProgress := progressStart + int(rawProgress*float64(progressEnd-progressStart))
|
||||||
progressCallback(scaledProgress)
|
progressCallback(scaledProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if totalSize > 0 {
|
||||||
|
percent := float64(downloaded) * 100 / float64(totalSize)
|
||||||
|
if speedMBps > 0 {
|
||||||
|
fmt.Printf("\r[FFmpeg] Downloading: %.2f MB / %.2f MB (%.1f%%) - %.2f MB/s",
|
||||||
|
mbDownloaded, float64(totalSize)/(1024*1024), percent, speedMBps)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\r[FFmpeg] Downloading: %.2f MB / %.2f MB (%.1f%%)",
|
||||||
|
mbDownloaded, float64(totalSize)/(1024*1024), percent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if speedMBps > 0 {
|
||||||
|
fmt.Printf("\r[FFmpeg] Downloading: %.2f MB - %.2f MB/s", mbDownloaded, speedMBps)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\r[FFmpeg] Downloading: %.2f MB", mbDownloaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
@@ -239,16 +564,23 @@ func downloadAndExtract(url, destDir string, progressCallback func(int), progres
|
|||||||
|
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
fmt.Printf("[FFmpeg] Download complete, extracting...\n")
|
if totalSize > 0 {
|
||||||
|
fmt.Printf("\r[FFmpeg] Download complete: %.2f MB / %.2f MB (100%%) \n",
|
||||||
|
float64(downloaded)/(1024*1024), float64(totalSize)/(1024*1024))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\r[FFmpeg] Download complete: %.2f MB \n", float64(downloaded)/(1024*1024))
|
||||||
|
}
|
||||||
|
fmt.Printf("[FFmpeg] Extracting...\n")
|
||||||
|
|
||||||
// Extract the archive based on file type
|
if strings.HasSuffix(url, ".tar.xz") {
|
||||||
if strings.HasSuffix(url, ".tar.xz") || runtime.GOOS == "linux" {
|
|
||||||
return extractTarXz(tmpFile.Name(), destDir)
|
return extractTarXz(tmpFile.Name(), destDir)
|
||||||
}
|
}
|
||||||
|
if strings.HasSuffix(url, ".zip") {
|
||||||
return extractZip(tmpFile.Name(), destDir)
|
return extractZip(tmpFile.Name(), destDir)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unsupported archive format for %s", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractZip extracts ffmpeg and ffprobe from a zip archive (skips ffplay)
|
|
||||||
func extractZip(zipPath, destDir string) error {
|
func extractZip(zipPath, destDir string) error {
|
||||||
r, err := zip.OpenReader(zipPath)
|
r, err := zip.OpenReader(zipPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -280,7 +612,7 @@ func extractZip(zipPath, destDir string) error {
|
|||||||
destPath = filepath.Join(destDir, ffprobeName)
|
destPath = filepath.Join(destDir, ffprobeName)
|
||||||
foundFFprobe = true
|
foundFFprobe = true
|
||||||
} else {
|
} else {
|
||||||
// Skip ffplay and other files
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,10 +637,13 @@ func extractZip(zipPath, destDir string) error {
|
|||||||
return fmt.Errorf("failed to extract file: %w", err)
|
return fmt.Errorf("failed to extract file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := prepareExecutableForUse(destPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to prepare extracted executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("[FFmpeg] Extracted to: %s\n", destPath)
|
fmt.Printf("[FFmpeg] Extracted to: %s\n", destPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// At least one of ffmpeg or ffprobe should be found
|
|
||||||
if !foundFFmpeg && !foundFFprobe {
|
if !foundFFmpeg && !foundFFprobe {
|
||||||
return fmt.Errorf("neither ffmpeg nor ffprobe found in archive")
|
return fmt.Errorf("neither ffmpeg nor ffprobe found in archive")
|
||||||
}
|
}
|
||||||
@@ -323,7 +658,6 @@ func extractZip(zipPath, destDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractTarXz extracts ffmpeg and ffprobe from a tar.xz archive (skips ffplay)
|
|
||||||
func extractTarXz(tarXzPath, destDir string) error {
|
func extractTarXz(tarXzPath, destDir string) error {
|
||||||
file, err := os.Open(tarXzPath)
|
file, err := os.Open(tarXzPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -366,7 +700,7 @@ func extractTarXz(tarXzPath, destDir string) error {
|
|||||||
destPath = filepath.Join(destDir, ffprobeName)
|
destPath = filepath.Join(destDir, ffprobeName)
|
||||||
foundFFprobe = true
|
foundFFprobe = true
|
||||||
} else {
|
} else {
|
||||||
// Skip ffplay and other files
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,10 +718,13 @@ func extractTarXz(tarXzPath, destDir string) error {
|
|||||||
return fmt.Errorf("failed to extract file: %w", err)
|
return fmt.Errorf("failed to extract file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := prepareExecutableForUse(destPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to prepare extracted executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("[FFmpeg] Extracted to: %s\n", destPath)
|
fmt.Printf("[FFmpeg] Extracted to: %s\n", destPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// At least one of ffmpeg or ffprobe should be found
|
|
||||||
if !foundFFmpeg && !foundFFprobe {
|
if !foundFFmpeg && !foundFFprobe {
|
||||||
return fmt.Errorf("neither ffmpeg nor ffprobe found in archive")
|
return fmt.Errorf("neither ffmpeg nor ffprobe found in archive")
|
||||||
}
|
}
|
||||||
@@ -402,15 +739,13 @@ func extractTarXz(tarXzPath, destDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertAudioRequest represents a request to convert audio files
|
|
||||||
type ConvertAudioRequest struct {
|
type ConvertAudioRequest struct {
|
||||||
InputFiles []string `json:"input_files"`
|
InputFiles []string `json:"input_files"`
|
||||||
OutputFormat string `json:"output_format"` // mp3, m4a
|
OutputFormat string `json:"output_format"`
|
||||||
Bitrate string `json:"bitrate"` // e.g., "320k", "256k", "192k", "128k" (ignored for ALAC)
|
Bitrate string `json:"bitrate"`
|
||||||
Codec string `json:"codec"` // For m4a: "aac" (lossy) or "alac" (lossless). Default: "aac"
|
Codec string `json:"codec"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertAudioResult represents the result of a single file conversion
|
|
||||||
type ConvertAudioResult struct {
|
type ConvertAudioResult struct {
|
||||||
InputFile string `json:"input_file"`
|
InputFile string `json:"input_file"`
|
||||||
OutputFile string `json:"output_file"`
|
OutputFile string `json:"output_file"`
|
||||||
@@ -418,13 +753,16 @@ type ConvertAudioResult struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertAudio converts audio files using ffmpeg while preserving metadata
|
|
||||||
func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
||||||
ffmpegPath, err := GetFFmpegPath()
|
ffmpegPath, err := GetFFmpegPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get ffmpeg path: %w", err)
|
return nil, fmt.Errorf("failed to get ffmpeg path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ValidateExecutable(ffmpegPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ffmpeg executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
installed, err := IsFFmpegInstalled()
|
installed, err := IsFFmpegInstalled()
|
||||||
if err != nil || !installed {
|
if err != nil || !installed {
|
||||||
return nil, fmt.Errorf("ffmpeg is not installed")
|
return nil, fmt.Errorf("ffmpeg is not installed")
|
||||||
@@ -434,7 +772,6 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
|
|
||||||
// Convert files in parallel
|
|
||||||
for i, inputFile := range req.InputFiles {
|
for i, inputFile := range req.InputFiles {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(idx int, inputFile string) {
|
go func(idx int, inputFile string) {
|
||||||
@@ -444,16 +781,13 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
InputFile: inputFile,
|
InputFile: inputFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get input file info
|
|
||||||
inputExt := strings.ToLower(filepath.Ext(inputFile))
|
inputExt := strings.ToLower(filepath.Ext(inputFile))
|
||||||
baseName := strings.TrimSuffix(filepath.Base(inputFile), inputExt)
|
baseName := strings.TrimSuffix(filepath.Base(inputFile), inputExt)
|
||||||
inputDir := filepath.Dir(inputFile)
|
inputDir := filepath.Dir(inputFile)
|
||||||
|
|
||||||
// Determine output directory: same as input file location + subfolder (MP3 or M4A)
|
|
||||||
outputFormatUpper := strings.ToUpper(req.OutputFormat)
|
outputFormatUpper := strings.ToUpper(req.OutputFormat)
|
||||||
outputDir := filepath.Join(inputDir, outputFormatUpper)
|
outputDir := filepath.Join(inputDir, outputFormatUpper)
|
||||||
|
|
||||||
// Create output directory if it doesn't exist
|
|
||||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
result.Error = fmt.Sprintf("failed to create output directory: %v", err)
|
result.Error = fmt.Sprintf("failed to create output directory: %v", err)
|
||||||
result.Success = false
|
result.Success = false
|
||||||
@@ -463,11 +797,10 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine output path
|
|
||||||
outputExt := "." + strings.ToLower(req.OutputFormat)
|
outputExt := "." + strings.ToLower(req.OutputFormat)
|
||||||
outputFile := filepath.Join(outputDir, baseName+outputExt)
|
outputFile := filepath.Join(outputDir, baseName+outputExt)
|
||||||
|
outputFile = norm.NFC.String(outputFile)
|
||||||
|
|
||||||
// Skip if same format
|
|
||||||
if inputExt == outputExt {
|
if inputExt == outputExt {
|
||||||
result.Error = "Input and output formats are the same"
|
result.Error = "Input and output formats are the same"
|
||||||
result.Success = false
|
result.Success = false
|
||||||
@@ -479,11 +812,20 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
|
|
||||||
result.OutputFile = outputFile
|
result.OutputFile = outputFile
|
||||||
|
|
||||||
// Extract cover art and lyrics from input file before conversion
|
|
||||||
var coverArtPath string
|
var coverArtPath string
|
||||||
var lyrics string
|
var lyrics string
|
||||||
|
var inputMetadata Metadata
|
||||||
|
|
||||||
coverArtPath, _ = ExtractCoverArt(inputFile)
|
inputMetadata, err = ExtractFullMetadataFromFile(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[FFmpeg] Warning: Failed to extract metadata from %s: %v\n", inputFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFile = norm.NFC.String(inputFile)
|
||||||
|
coverArtPath, err = ExtractCoverArt(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[FFmpeg] Warning: Failed to extract cover art from %s: %v\n", inputFile, err)
|
||||||
|
}
|
||||||
lyrics, err = ExtractLyrics(inputFile)
|
lyrics, err = ExtractLyrics(inputFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[FFmpeg] Warning: Failed to extract lyrics from %s: %v\n", inputFile, err)
|
fmt.Printf("[FFmpeg] Warning: Failed to extract lyrics from %s: %v\n", inputFile, err)
|
||||||
@@ -493,49 +835,72 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
fmt.Printf("[FFmpeg] No lyrics found in %s\n", inputFile)
|
fmt.Printf("[FFmpeg] No lyrics found in %s\n", inputFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build ffmpeg command
|
inputMetadata.Lyrics = lyrics
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-i", inputFile,
|
"-i", inputFile,
|
||||||
"-y", // Overwrite output
|
"-y",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add codec and bitrate based on output format
|
|
||||||
switch req.OutputFormat {
|
switch req.OutputFormat {
|
||||||
case "mp3":
|
case "mp3":
|
||||||
args = append(args,
|
args = append(args,
|
||||||
"-codec:a", "libmp3lame",
|
"-codec:a", "libmp3lame",
|
||||||
"-b:a", req.Bitrate,
|
"-b:a", req.Bitrate,
|
||||||
"-map", "0:a", // Map audio stream
|
"-map", "0:a",
|
||||||
"-map_metadata", "0", // Copy all metadata
|
"-id3v2_version", "3",
|
||||||
"-id3v2_version", "3", // Use ID3v2.3 for better compatibility
|
|
||||||
)
|
)
|
||||||
// Map video stream if exists (for cover art)
|
|
||||||
args = append(args, "-map", "0:v?", "-c:v", "copy")
|
|
||||||
case "m4a":
|
case "m4a":
|
||||||
// Determine codec: ALAC (lossless) or AAC (lossy)
|
|
||||||
codec := req.Codec
|
codec := req.Codec
|
||||||
if codec == "" {
|
if codec == "" {
|
||||||
codec = "aac" // Default to AAC for backward compatibility
|
codec = "aac"
|
||||||
}
|
}
|
||||||
|
|
||||||
if codec == "alac" {
|
if codec == "alac" {
|
||||||
// ALAC - Apple Lossless (no bitrate needed)
|
|
||||||
args = append(args,
|
args = append(args,
|
||||||
"-codec:a", "alac",
|
"-codec:a", "alac",
|
||||||
"-map", "0:a", // Map audio stream
|
"-map", "0:a",
|
||||||
"-map_metadata", "0", // Copy all metadata
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// AAC - lossy with bitrate
|
|
||||||
args = append(args,
|
args = append(args,
|
||||||
"-codec:a", "aac",
|
"-codec:a", "aac",
|
||||||
"-b:a", req.Bitrate,
|
"-b:a", req.Bitrate,
|
||||||
"-map", "0:a", // Map audio stream
|
"-map", "0:a",
|
||||||
"-map_metadata", "0", // Copy all metadata
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Map video stream for cover art in M4A
|
case "wav", "aiff":
|
||||||
args = append(args, "-map", "0:v?", "-c:v", "copy", "-disposition:v:0", "attached_pic")
|
sampleFmt, rawBits := pcmSampleFormatForInput(inputFile)
|
||||||
|
pcmCodec := "pcm_s16le"
|
||||||
|
if req.OutputFormat == "aiff" {
|
||||||
|
pcmCodec = "pcm_s16be"
|
||||||
|
}
|
||||||
|
if sampleFmt == "s32" {
|
||||||
|
if req.OutputFormat == "aiff" {
|
||||||
|
pcmCodec = "pcm_s24be"
|
||||||
|
} else {
|
||||||
|
pcmCodec = "pcm_s24le"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args = append(args,
|
||||||
|
"-codec:a", pcmCodec,
|
||||||
|
"-map", "0:a",
|
||||||
|
)
|
||||||
|
if rawBits > 0 {
|
||||||
|
args = append(args, "-bits_per_raw_sample", strconv.Itoa(rawBits))
|
||||||
|
}
|
||||||
|
case "opus":
|
||||||
|
bitrate := req.Bitrate
|
||||||
|
if bitrate == "" {
|
||||||
|
bitrate = "192k"
|
||||||
|
}
|
||||||
|
args = append(args,
|
||||||
|
"-codec:a", "libopus",
|
||||||
|
"-b:a", bitrate,
|
||||||
|
"-map", "0:a",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, outputFile)
|
args = append(args, outputFile)
|
||||||
@@ -543,7 +908,7 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
fmt.Printf("[FFmpeg] Converting: %s -> %s\n", inputFile, outputFile)
|
fmt.Printf("[FFmpeg] Converting: %s -> %s\n", inputFile, outputFile)
|
||||||
|
|
||||||
cmd := exec.Command(ffmpegPath, args...)
|
cmd := exec.Command(ffmpegPath, args...)
|
||||||
// Hide console window on Windows
|
|
||||||
setHideWindow(cmd)
|
setHideWindow(cmd)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -552,21 +917,17 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
mu.Lock()
|
mu.Lock()
|
||||||
results[idx] = result
|
results[idx] = result
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
// Clean up temp cover art file if exists
|
|
||||||
if coverArtPath != "" {
|
if coverArtPath != "" {
|
||||||
os.Remove(coverArtPath)
|
os.Remove(coverArtPath)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embed cover art and lyrics after conversion if they were extracted
|
if err := EmbedMetadataToConvertedFile(outputFile, inputMetadata, coverArtPath); err != nil {
|
||||||
if coverArtPath != "" {
|
fmt.Printf("[FFmpeg] Warning: Failed to embed metadata: %v\n", err)
|
||||||
if err := EmbedCoverArtOnly(outputFile, coverArtPath); err != nil {
|
|
||||||
fmt.Printf("[FFmpeg] Warning: Failed to embed cover art: %v\n", err)
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("[FFmpeg] Cover art embedded successfully\n")
|
fmt.Printf("[FFmpeg] Metadata embedded successfully\n")
|
||||||
}
|
|
||||||
os.Remove(coverArtPath) // Clean up temp file
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if lyrics != "" {
|
if lyrics != "" {
|
||||||
@@ -577,6 +938,10 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if coverArtPath != "" {
|
||||||
|
os.Remove(coverArtPath)
|
||||||
|
}
|
||||||
|
|
||||||
result.Success = true
|
result.Success = true
|
||||||
fmt.Printf("[FFmpeg] Successfully converted: %s\n", outputFile)
|
fmt.Printf("[FFmpeg] Successfully converted: %s\n", outputFile)
|
||||||
|
|
||||||
@@ -590,7 +955,13 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAudioInfo returns information about an audio file
|
func pcmSampleFormatForInput(inputFile string) (sampleFmt string, rawBits int) {
|
||||||
|
if meta, err := GetTrackMetadata(inputFile); err == nil && meta != nil && meta.BitsPerSample > 16 {
|
||||||
|
return "s32", 24
|
||||||
|
}
|
||||||
|
return "s16", 0
|
||||||
|
}
|
||||||
|
|
||||||
type AudioFileInfo struct {
|
type AudioFileInfo struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
@@ -598,7 +969,6 @@ type AudioFileInfo struct {
|
|||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAudioFileInfo gets information about an audio file
|
|
||||||
func GetAudioFileInfo(filePath string) (*AudioFileInfo, error) {
|
func GetAudioFileInfo(filePath string) (*AudioFileInfo, error) {
|
||||||
info, err := os.Stat(filePath)
|
info, err := os.Stat(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setHideWindow is a no-op on non-Windows platforms
|
|
||||||
func setHideWindow(cmd *exec.Cmd) {
|
func setHideWindow(cmd *exec.Cmd) {
|
||||||
// No-op on Unix-like systems
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setHideWindow sets HideWindow attribute for Windows processes
|
|
||||||
func setHideWindow(cmd *exec.Cmd) {
|
func setHideWindow(cmd *exec.Cmd) {
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
HideWindow: true,
|
HideWindow: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SelectMultipleFiles opens a file dialog to select multiple audio files
|
|
||||||
func SelectMultipleFiles(ctx context.Context) ([]string, error) {
|
func SelectMultipleFiles(ctx context.Context) ([]string, error) {
|
||||||
files, err := runtime.OpenMultipleFilesDialog(ctx, runtime.OpenDialogOptions{
|
files, err := runtime.OpenMultipleFilesDialog(ctx, runtime.OpenDialogOptions{
|
||||||
Title: "Select Audio Files",
|
Title: "Select Audio Files",
|
||||||
Filters: []runtime.FileFilter{
|
Filters: []runtime.FileFilter{
|
||||||
{
|
{
|
||||||
DisplayName: "Audio Files (*.mp3, *.m4a, *.flac)",
|
DisplayName: "Audio Files (*.mp3, *.m4a, *.flac, *.aac)",
|
||||||
Pattern: "*.mp3;*.m4a;*.flac",
|
Pattern: "*.mp3;*.m4a;*.flac;*.aac",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
DisplayName: "MP3 Files (*.mp3)",
|
DisplayName: "MP3 Files (*.mp3)",
|
||||||
@@ -27,6 +26,10 @@ func SelectMultipleFiles(ctx context.Context) ([]string, error) {
|
|||||||
DisplayName: "FLAC Files (*.flac)",
|
DisplayName: "FLAC Files (*.flac)",
|
||||||
Pattern: "*.flac",
|
Pattern: "*.flac",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "AAC Files (*.aac)",
|
||||||
|
Pattern: "*.aac",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
DisplayName: "All Files (*.*)",
|
DisplayName: "All Files (*.*)",
|
||||||
Pattern: "*.*",
|
Pattern: "*.*",
|
||||||
@@ -39,7 +42,6 @@ func SelectMultipleFiles(ctx context.Context) ([]string, error) {
|
|||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelectOutputDirectory opens a directory dialog to select output folder
|
|
||||||
func SelectOutputDirectory(ctx context.Context) (string, error) {
|
func SelectOutputDirectory(ctx context.Context) (string, error) {
|
||||||
dir, err := runtime.OpenDirectoryDialog(ctx, runtime.OpenDialogOptions{
|
dir, err := runtime.OpenDirectoryDialog(ctx, runtime.OpenDialogOptions{
|
||||||
Title: "Select Output Directory",
|
Title: "Select Output Directory",
|
||||||
@@ -49,4 +51,3 @@ func SelectOutputDirectory(ctx context.Context) (string, error) {
|
|||||||
}
|
}
|
||||||
return dir, nil
|
return dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetMacOSFileIconFromImage(filePath, imagePath string, iconSize int) error {
|
||||||
|
if filePath == "" {
|
||||||
|
return fmt.Errorf("file path is required")
|
||||||
|
}
|
||||||
|
if imagePath == "" {
|
||||||
|
return fmt.Errorf("image path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
resizedPath, err := ResizeImageForIcon(imagePath, iconSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(resizedPath)
|
||||||
|
|
||||||
|
script := `
|
||||||
|
use framework "AppKit"
|
||||||
|
on run argv
|
||||||
|
set imagePath to item 1 of argv
|
||||||
|
set targetPath to item 2 of argv
|
||||||
|
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:imagePath
|
||||||
|
if iconImage is missing value then error "Failed to load icon image"
|
||||||
|
set didSet to (current application's NSWorkspace's sharedWorkspace()'s setIcon:iconImage forFile:targetPath options:0) as boolean
|
||||||
|
if didSet is false then error "Failed to set custom file icon"
|
||||||
|
end run
|
||||||
|
`
|
||||||
|
|
||||||
|
cmd := exec.Command("osascript", "-", resizedPath, filePath)
|
||||||
|
cmd.Stdin = strings.NewReader(script)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to apply macOS file icon: %v (%s)", err, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !darwin
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
func SetMacOSFileIconFromImage(filePath, imagePath string, iconSize int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/go-flac/go-flac"
|
"github.com/go-flac/go-flac"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileInfo represents information about a file or folder
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -23,7 +22,6 @@ type FileInfo struct {
|
|||||||
Children []FileInfo `json:"children,omitempty"`
|
Children []FileInfo `json:"children,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioMetadata represents metadata read from an audio file
|
|
||||||
type AudioMetadata struct {
|
type AudioMetadata struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Artist string `json:"artist"`
|
Artist string `json:"artist"`
|
||||||
@@ -32,9 +30,10 @@ type AudioMetadata struct {
|
|||||||
TrackNumber int `json:"track_number"`
|
TrackNumber int `json:"track_number"`
|
||||||
DiscNumber int `json:"disc_number"`
|
DiscNumber int `json:"disc_number"`
|
||||||
Year string `json:"year"`
|
Year string `json:"year"`
|
||||||
|
ISRC string `json:"isrc"`
|
||||||
|
UPC string `json:"upc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenamePreview represents a preview of file rename operation
|
|
||||||
type RenamePreview struct {
|
type RenamePreview struct {
|
||||||
OldPath string `json:"old_path"`
|
OldPath string `json:"old_path"`
|
||||||
OldName string `json:"old_name"`
|
OldName string `json:"old_name"`
|
||||||
@@ -44,7 +43,6 @@ type RenamePreview struct {
|
|||||||
Metadata AudioMetadata `json:"metadata"`
|
Metadata AudioMetadata `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenameResult represents the result of a rename operation
|
|
||||||
type RenameResult struct {
|
type RenameResult struct {
|
||||||
OldPath string `json:"old_path"`
|
OldPath string `json:"old_path"`
|
||||||
NewPath string `json:"new_path"`
|
NewPath string `json:"new_path"`
|
||||||
@@ -52,7 +50,6 @@ type RenameResult struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDirectory lists files and folders in a directory
|
|
||||||
func ListDirectory(dirPath string) ([]FileInfo, error) {
|
func ListDirectory(dirPath string) ([]FileInfo, error) {
|
||||||
entries, err := os.ReadDir(dirPath)
|
entries, err := os.ReadDir(dirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -73,7 +70,6 @@ func ListDirectory(dirPath string) ([]FileInfo, error) {
|
|||||||
Size: info.Size(),
|
Size: info.Size(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a directory, recursively list its contents
|
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
children, err := ListDirectory(fileInfo.Path)
|
children, err := ListDirectory(fileInfo.Path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -87,13 +83,12 @@ func ListDirectory(dirPath string) ([]FileInfo, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListAudioFiles lists only audio files (flac, mp3, m4a) in a directory recursively
|
|
||||||
func ListAudioFiles(dirPath string) ([]FileInfo, error) {
|
func ListAudioFiles(dirPath string) ([]FileInfo, error) {
|
||||||
var result []FileInfo
|
var result []FileInfo
|
||||||
|
|
||||||
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil // Skip files with errors
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
@@ -101,7 +96,7 @@ func ListAudioFiles(dirPath string) ([]FileInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ext := strings.ToLower(filepath.Ext(path))
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
if ext == ".flac" || ext == ".mp3" || ext == ".m4a" {
|
if ext == ".flac" || ext == ".mp3" || ext == ".m4a" || ext == ".aac" {
|
||||||
result = append(result, FileInfo{
|
result = append(result, FileInfo{
|
||||||
Name: info.Name(),
|
Name: info.Name(),
|
||||||
Path: path,
|
Path: path,
|
||||||
@@ -120,7 +115,6 @@ func ListAudioFiles(dirPath string) ([]FileInfo, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadAudioMetadata reads metadata from an audio file
|
|
||||||
func ReadAudioMetadata(filePath string) (*AudioMetadata, error) {
|
func ReadAudioMetadata(filePath string) (*AudioMetadata, error) {
|
||||||
if !fileExists(filePath) {
|
if !fileExists(filePath) {
|
||||||
return nil, fmt.Errorf("file does not exist")
|
return nil, fmt.Errorf("file does not exist")
|
||||||
@@ -140,7 +134,6 @@ func ReadAudioMetadata(filePath string) (*AudioMetadata, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// readFlacMetadata reads metadata from a FLAC file
|
|
||||||
func readFlacMetadata(filePath string) (*AudioMetadata, error) {
|
func readFlacMetadata(filePath string) (*AudioMetadata, error) {
|
||||||
f, err := flac.ParseFile(filePath)
|
f, err := flac.ParseFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -184,6 +177,12 @@ func readFlacMetadata(filePath string) (*AudioMetadata, error) {
|
|||||||
}
|
}
|
||||||
case "DATE", "YEAR":
|
case "DATE", "YEAR":
|
||||||
metadata.Year = value
|
metadata.Year = value
|
||||||
|
case "ISRC", "TSRC":
|
||||||
|
metadata.ISRC = value
|
||||||
|
case "UPC":
|
||||||
|
assignPreferredUPC(&metadata.UPC, value, true)
|
||||||
|
case "BARCODE":
|
||||||
|
assignPreferredUPC(&metadata.UPC, value, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,7 +191,6 @@ func readFlacMetadata(filePath string) (*AudioMetadata, error) {
|
|||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readMp3Metadata reads metadata from an MP3 file
|
|
||||||
func readMp3Metadata(filePath string) (*AudioMetadata, error) {
|
func readMp3Metadata(filePath string) (*AudioMetadata, error) {
|
||||||
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
|
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -207,14 +205,12 @@ func readMp3Metadata(filePath string) (*AudioMetadata, error) {
|
|||||||
Year: tag.Year(),
|
Year: tag.Year(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Album Artist (TPE2)
|
|
||||||
if frames := tag.GetFrames("TPE2"); len(frames) > 0 {
|
if frames := tag.GetFrames("TPE2"); len(frames) > 0 {
|
||||||
if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
|
if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
|
||||||
metadata.AlbumArtist = textFrame.Text
|
metadata.AlbumArtist = textFrame.Text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Track Number
|
|
||||||
if frames := tag.GetFrames(tag.CommonID("Track number/Position in set")); len(frames) > 0 {
|
if frames := tag.GetFrames(tag.CommonID("Track number/Position in set")); len(frames) > 0 {
|
||||||
if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
|
if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
|
||||||
trackStr := strings.Split(textFrame.Text, "/")[0]
|
trackStr := strings.Split(textFrame.Text, "/")[0]
|
||||||
@@ -224,7 +220,6 @@ func readMp3Metadata(filePath string) (*AudioMetadata, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Disc Number
|
|
||||||
if frames := tag.GetFrames(tag.CommonID("Part of a set")); len(frames) > 0 {
|
if frames := tag.GetFrames(tag.CommonID("Part of a set")); len(frames) > 0 {
|
||||||
if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
|
if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
|
||||||
discStr := strings.Split(textFrame.Text, "/")[0]
|
discStr := strings.Split(textFrame.Text, "/")[0]
|
||||||
@@ -234,17 +229,41 @@ func readMp3Metadata(filePath string) (*AudioMetadata, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if frames := tag.GetFrames("TSRC"); len(frames) > 0 {
|
||||||
|
if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
|
||||||
|
metadata.ISRC = textFrame.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if frames := tag.GetFrames("TXXX"); len(frames) > 0 {
|
||||||
|
for _, frame := range frames {
|
||||||
|
userTextFrame, ok := frame.(id3v2.UserDefinedTextFrame)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matched, preferred := classifyUPCDescription(userTextFrame.Description)
|
||||||
|
if !matched {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assignPreferredUPC(&metadata.UPC, userTextFrame.Value, preferred)
|
||||||
|
if preferred && strings.TrimSpace(metadata.UPC) != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readMetadataWithFFprobe reads metadata from any audio file using ffprobe
|
|
||||||
func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
|
func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
|
||||||
ffprobePath, err := GetFFprobePath()
|
ffprobePath, err := GetFFprobePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use ffprobe to get metadata in JSON format (both format and stream tags)
|
if err := ValidateExecutable(ffprobePath); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ffprobe executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.Command(ffprobePath,
|
cmd := exec.Command(ffprobePath,
|
||||||
"-v", "quiet",
|
"-v", "quiet",
|
||||||
"-print_format", "json",
|
"-print_format", "json",
|
||||||
@@ -253,7 +272,6 @@ func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
|
|||||||
filePath,
|
filePath,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hide console window on Windows
|
|
||||||
setHideWindow(cmd)
|
setHideWindow(cmd)
|
||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
@@ -261,7 +279,6 @@ func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse JSON output
|
|
||||||
var result struct {
|
var result struct {
|
||||||
Format struct {
|
Format struct {
|
||||||
Tags map[string]string `json:"tags"`
|
Tags map[string]string `json:"tags"`
|
||||||
@@ -277,22 +294,18 @@ func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
|
|||||||
|
|
||||||
metadata := &AudioMetadata{}
|
metadata := &AudioMetadata{}
|
||||||
|
|
||||||
// Merge tags from format and streams (format tags take priority)
|
|
||||||
allTags := make(map[string]string)
|
allTags := make(map[string]string)
|
||||||
|
|
||||||
// First add stream tags
|
|
||||||
for _, stream := range result.Streams {
|
for _, stream := range result.Streams {
|
||||||
for key, value := range stream.Tags {
|
for key, value := range stream.Tags {
|
||||||
allTags[strings.ToLower(key)] = value
|
allTags[strings.ToLower(key)] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then add format tags (overwrite stream tags)
|
|
||||||
for key, value := range result.Format.Tags {
|
for key, value := range result.Format.Tags {
|
||||||
allTags[strings.ToLower(key)] = value
|
allTags[strings.ToLower(key)] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse tags
|
|
||||||
for key, value := range allTags {
|
for key, value := range allTags {
|
||||||
switch key {
|
switch key {
|
||||||
case "title":
|
case "title":
|
||||||
@@ -304,7 +317,7 @@ func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
|
|||||||
case "album_artist", "albumartist":
|
case "album_artist", "albumartist":
|
||||||
metadata.AlbumArtist = value
|
metadata.AlbumArtist = value
|
||||||
case "track":
|
case "track":
|
||||||
// Format might be "4" or "4/12"
|
|
||||||
trackStr := strings.Split(value, "/")[0]
|
trackStr := strings.Split(value, "/")[0]
|
||||||
if num, err := strconv.Atoi(trackStr); err == nil {
|
if num, err := strconv.Atoi(trackStr); err == nil {
|
||||||
metadata.TrackNumber = num
|
metadata.TrackNumber = num
|
||||||
@@ -318,13 +331,16 @@ func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
|
|||||||
if metadata.Year == "" || len(value) > len(metadata.Year) {
|
if metadata.Year == "" || len(value) > len(metadata.Year) {
|
||||||
metadata.Year = value
|
metadata.Year = value
|
||||||
}
|
}
|
||||||
|
case "isrc", "tsrc":
|
||||||
|
metadata.ISRC = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metadata.UPC = firstPreferredFFprobeUPCValue(allTags)
|
||||||
|
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readM4aMetadata reads metadata from an M4A file using ffprobe
|
|
||||||
func readM4aMetadata(filePath string) (*AudioMetadata, error) {
|
func readM4aMetadata(filePath string) (*AudioMetadata, error) {
|
||||||
metadata, err := readMetadataWithFFprobe(filePath)
|
metadata, err := readMetadataWithFFprobe(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -333,7 +349,6 @@ func readM4aMetadata(filePath string) (*AudioMetadata, error) {
|
|||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateFilename generates a new filename based on metadata and format template
|
|
||||||
func GenerateFilename(metadata *AudioMetadata, format string, ext string) string {
|
func GenerateFilename(metadata *AudioMetadata, format string, ext string) string {
|
||||||
if metadata == nil {
|
if metadata == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -341,32 +356,34 @@ func GenerateFilename(metadata *AudioMetadata, format string, ext string) string
|
|||||||
|
|
||||||
result := format
|
result := format
|
||||||
|
|
||||||
// Replace placeholders
|
year := metadata.Year
|
||||||
|
if len(year) >= 4 {
|
||||||
|
year = year[:4]
|
||||||
|
}
|
||||||
|
|
||||||
result = strings.ReplaceAll(result, "{title}", sanitizeFilenameForRename(metadata.Title))
|
result = strings.ReplaceAll(result, "{title}", sanitizeFilenameForRename(metadata.Title))
|
||||||
result = strings.ReplaceAll(result, "{artist}", sanitizeFilenameForRename(metadata.Artist))
|
result = strings.ReplaceAll(result, "{artist}", sanitizeFilenameForRename(metadata.Artist))
|
||||||
result = strings.ReplaceAll(result, "{album}", sanitizeFilenameForRename(metadata.Album))
|
result = strings.ReplaceAll(result, "{album}", sanitizeFilenameForRename(metadata.Album))
|
||||||
result = strings.ReplaceAll(result, "{album_artist}", sanitizeFilenameForRename(metadata.AlbumArtist))
|
result = strings.ReplaceAll(result, "{album_artist}", sanitizeFilenameForRename(metadata.AlbumArtist))
|
||||||
result = strings.ReplaceAll(result, "{year}", sanitizeFilenameForRename(metadata.Year))
|
result = strings.ReplaceAll(result, "{year}", sanitizeFilenameForRename(year))
|
||||||
|
result = strings.ReplaceAll(result, "{date}", sanitizeFilenameForRename(metadata.Year))
|
||||||
|
result = strings.ReplaceAll(result, "{isrc}", sanitizeFilenameForRename(metadata.ISRC))
|
||||||
|
|
||||||
// Track number with padding
|
|
||||||
if metadata.TrackNumber > 0 {
|
if metadata.TrackNumber > 0 {
|
||||||
result = strings.ReplaceAll(result, "{track}", fmt.Sprintf("%02d", metadata.TrackNumber))
|
result = strings.ReplaceAll(result, "{track}", fmt.Sprintf("%02d", metadata.TrackNumber))
|
||||||
} else {
|
} else {
|
||||||
result = strings.ReplaceAll(result, "{track}", "")
|
result = strings.ReplaceAll(result, "{track}", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disc number
|
|
||||||
if metadata.DiscNumber > 0 {
|
if metadata.DiscNumber > 0 {
|
||||||
result = strings.ReplaceAll(result, "{disc}", fmt.Sprintf("%d", metadata.DiscNumber))
|
result = strings.ReplaceAll(result, "{disc}", fmt.Sprintf("%d", metadata.DiscNumber))
|
||||||
} else {
|
} else {
|
||||||
result = strings.ReplaceAll(result, "{disc}", "")
|
result = strings.ReplaceAll(result, "{disc}", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up multiple spaces and trim
|
|
||||||
result = strings.TrimSpace(result)
|
result = strings.TrimSpace(result)
|
||||||
result = strings.Join(strings.Fields(result), " ")
|
result = strings.Join(strings.Fields(result), " ")
|
||||||
|
|
||||||
// Remove leading/trailing separators
|
|
||||||
result = strings.Trim(result, " -._")
|
result = strings.Trim(result, " -._")
|
||||||
|
|
||||||
if result == "" {
|
if result == "" {
|
||||||
@@ -376,9 +393,8 @@ func GenerateFilename(metadata *AudioMetadata, format string, ext string) string
|
|||||||
return result + ext
|
return result + ext
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeFilenameForRename removes invalid characters from filename (for rename operations)
|
|
||||||
func sanitizeFilenameForRename(name string) string {
|
func sanitizeFilenameForRename(name string) string {
|
||||||
// Remove characters that are invalid in filenames
|
|
||||||
invalid := []string{"<", ">", ":", "\"", "/", "\\", "|", "?", "*"}
|
invalid := []string{"<", ">", ":", "\"", "/", "\\", "|", "?", "*"}
|
||||||
result := name
|
result := name
|
||||||
for _, char := range invalid {
|
for _, char := range invalid {
|
||||||
@@ -387,7 +403,6 @@ func sanitizeFilenameForRename(name string) string {
|
|||||||
return strings.TrimSpace(result)
|
return strings.TrimSpace(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreviewRename generates a preview of rename operations
|
|
||||||
func PreviewRename(files []string, format string) []RenamePreview {
|
func PreviewRename(files []string, format string) []RenamePreview {
|
||||||
var previews []RenamePreview
|
var previews []RenamePreview
|
||||||
|
|
||||||
@@ -424,7 +439,6 @@ func PreviewRename(files []string, format string) []RenamePreview {
|
|||||||
return previews
|
return previews
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFileSizes returns file sizes for a list of file paths
|
|
||||||
func GetFileSizes(files []string) map[string]int64 {
|
func GetFileSizes(files []string) map[string]int64 {
|
||||||
result := make(map[string]int64)
|
result := make(map[string]int64)
|
||||||
for _, filePath := range files {
|
for _, filePath := range files {
|
||||||
@@ -436,7 +450,6 @@ func GetFileSizes(files []string) map[string]int64 {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenameFiles renames files based on their metadata
|
|
||||||
func RenameFiles(files []string, format string) []RenameResult {
|
func RenameFiles(files []string, format string) []RenameResult {
|
||||||
var results []RenameResult
|
var results []RenameResult
|
||||||
|
|
||||||
@@ -466,7 +479,6 @@ func RenameFiles(files []string, format string) []RenameResult {
|
|||||||
newPath := filepath.Join(filepath.Dir(filePath), newName)
|
newPath := filepath.Join(filepath.Dir(filePath), newName)
|
||||||
result.NewPath = newPath
|
result.NewPath = newPath
|
||||||
|
|
||||||
// Check if new path already exists (and is different from old path)
|
|
||||||
if newPath != filePath {
|
if newPath != filePath {
|
||||||
if _, err := os.Stat(newPath); err == nil {
|
if _, err := os.Stat(newPath); err == nil {
|
||||||
result.Error = "File already exists"
|
result.Error = "File already exists"
|
||||||
@@ -476,7 +488,6 @@ func RenameFiles(files []string, format string) []RenameResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename the file
|
|
||||||
if err := os.Rename(filePath, newPath); err != nil {
|
if err := os.Rename(filePath, newPath); err != nil {
|
||||||
result.Error = err.Error()
|
result.Error = err.Error()
|
||||||
result.Success = false
|
result.Success = false
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package backend
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,152 +10,212 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BuildExpectedFilename builds the expected filename based on track metadata and settings
|
func buildFormattedFilenameBase(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, playlistName, playlistOwner, isrc string, includeTrackNumber bool, position, discNumber int, useAlbumTrackNumber bool) string {
|
||||||
func BuildExpectedFilename(trackName, artistName, filenameFormat string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string {
|
safeTitle := SanitizeFilename(trackName)
|
||||||
// Sanitize track name and artist name
|
safeArtist := SanitizeFilename(artistName)
|
||||||
safeTitle := sanitizeFilename(trackName)
|
safeAlbum := SanitizeFilename(albumName)
|
||||||
safeArtist := sanitizeFilename(artistName)
|
safeAlbumArtist := SanitizeFilename(albumArtist)
|
||||||
|
safeISRC := SanitizeOptionalFilename(isrc)
|
||||||
|
|
||||||
|
safePlaylist := SanitizeFilename(playlistName)
|
||||||
|
safeCreator := SanitizeFilename(playlistOwner)
|
||||||
|
|
||||||
|
year := ""
|
||||||
|
if len(releaseDate) >= 4 {
|
||||||
|
year = releaseDate[:4]
|
||||||
|
}
|
||||||
|
|
||||||
var filename string
|
var filename string
|
||||||
|
|
||||||
// Check if format is a template (contains {})
|
|
||||||
if strings.Contains(filenameFormat, "{") {
|
if strings.Contains(filenameFormat, "{") {
|
||||||
filename = filenameFormat
|
filename = filenameFormat
|
||||||
filename = strings.ReplaceAll(filename, "{title}", safeTitle)
|
filename = strings.ReplaceAll(filename, "{title}", safeTitle)
|
||||||
filename = strings.ReplaceAll(filename, "{artist}", safeArtist)
|
filename = strings.ReplaceAll(filename, "{artist}", safeArtist)
|
||||||
|
filename = strings.ReplaceAll(filename, "{album}", safeAlbum)
|
||||||
|
filename = strings.ReplaceAll(filename, "{album_artist}", safeAlbumArtist)
|
||||||
|
filename = strings.ReplaceAll(filename, "{year}", year)
|
||||||
|
filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate))
|
||||||
|
filename = strings.ReplaceAll(filename, "{playlist}", safePlaylist)
|
||||||
|
filename = strings.ReplaceAll(filename, "{creator}", safeCreator)
|
||||||
|
filename = strings.ReplaceAll(filename, "{isrc}", safeISRC)
|
||||||
|
|
||||||
|
if discNumber > 0 {
|
||||||
|
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
|
||||||
|
} else {
|
||||||
|
filename = strings.ReplaceAll(filename, "{disc}", "")
|
||||||
|
}
|
||||||
|
|
||||||
// Handle track number - if position is 0, remove {track} and surrounding separators
|
|
||||||
if position > 0 {
|
if position > 0 {
|
||||||
filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", position))
|
filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", position))
|
||||||
} else {
|
} else {
|
||||||
// Remove {track} with common separators like ". " or " - " or ". "
|
|
||||||
filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "")
|
filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "")
|
||||||
filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "")
|
filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "")
|
||||||
filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "")
|
filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy format support
|
|
||||||
switch filenameFormat {
|
switch filenameFormat {
|
||||||
case "artist-title":
|
case "artist-title":
|
||||||
filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle)
|
filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle)
|
||||||
case "title":
|
case "title":
|
||||||
filename = safeTitle
|
filename = safeTitle
|
||||||
default: // "title-artist"
|
default:
|
||||||
filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add track number prefix if enabled (legacy behavior)
|
|
||||||
if includeTrackNumber && position > 0 {
|
if includeTrackNumber && position > 0 {
|
||||||
filename = fmt.Sprintf("%02d. %s", position, filename)
|
filename = fmt.Sprintf("%02d. %s", position, filename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filename + ".flac"
|
return filename
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeFilename removes invalid characters from filename
|
func BuildExpectedFilename(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position, discNumber int, useAlbumTrackNumber bool, extra ...string) string {
|
||||||
func sanitizeFilename(name string) string {
|
isrc := ""
|
||||||
// Replace forward slash with space (more natural than underscore)
|
if len(extra) > 0 {
|
||||||
|
isrc = extra[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildFormattedFilenameBase(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, playlistName, playlistOwner, isrc, includeTrackNumber, position, discNumber, useAlbumTrackNumber) + ".flac"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveOutputPathForDownload(path string, redownloadWithSuffix bool) (string, bool) {
|
||||||
|
if !redownloadWithSuffix {
|
||||||
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
|
return path, true
|
||||||
|
}
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if info, err := os.Stat(path); err != nil || info.Size() == 0 {
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
base := strings.TrimSuffix(path, ext)
|
||||||
|
|
||||||
|
for i := 1; ; i++ {
|
||||||
|
candidate := fmt.Sprintf("%s_%02d%s", base, i, ext)
|
||||||
|
if info, err := os.Stat(candidate); err != nil || info.Size() == 0 {
|
||||||
|
return candidate, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustFileSize(path string) int64 {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return info.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizeFilename(name string) string {
|
||||||
|
|
||||||
sanitized := strings.ReplaceAll(name, "/", " ")
|
sanitized := strings.ReplaceAll(name, "/", " ")
|
||||||
|
|
||||||
// Remove other invalid filesystem characters (replace with space)
|
|
||||||
re := regexp.MustCompile(`[<>:"\\|?*]`)
|
re := regexp.MustCompile(`[<>:"\\|?*]`)
|
||||||
sanitized = re.ReplaceAllString(sanitized, " ")
|
sanitized = re.ReplaceAllString(sanitized, " ")
|
||||||
|
|
||||||
// Remove control characters (0x00-0x1F, 0x7F)
|
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
for _, r := range sanitized {
|
for _, r := range sanitized {
|
||||||
// Keep printable characters and valid Unicode characters
|
|
||||||
// Remove control characters, but keep spaces, tabs, newlines for now
|
|
||||||
if r < 0x20 && r != 0x09 && r != 0x0A && r != 0x0D {
|
if r < 0x20 && r != 0x09 && r != 0x0A && r != 0x0D {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if r == 0x7F {
|
if r == 0x7F {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Remove emoji and other symbols that might cause issues
|
|
||||||
// Keep letters, numbers, and common punctuation
|
|
||||||
if unicode.IsControl(r) && r != 0x09 && r != 0x0A && r != 0x0D {
|
if unicode.IsControl(r) && r != 0x09 && r != 0x0A && r != 0x0D {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Remove emoji ranges (most emoji are in these ranges)
|
|
||||||
if (r >= 0x1F300 && r <= 0x1F9FF) || // Miscellaneous Symbols and Pictographs, Emoticons
|
|
||||||
(r >= 0x2600 && r <= 0x26FF) || // Miscellaneous Symbols
|
|
||||||
(r >= 0x2700 && r <= 0x27BF) || // Dingbats
|
|
||||||
(r >= 0xFE00 && r <= 0xFE0F) || // Variation Selectors
|
|
||||||
(r >= 0x1F900 && r <= 0x1F9FF) || // Supplemental Symbols and Pictographs
|
|
||||||
(r >= 0x1F600 && r <= 0x1F64F) || // Emoticons
|
|
||||||
(r >= 0x1F680 && r <= 0x1F6FF) || // Transport and Map Symbols
|
|
||||||
(r >= 0x1F1E0 && r <= 0x1F1FF) { // Regional Indicator Symbols (flags)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result.WriteRune(r)
|
result.WriteRune(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
sanitized = result.String()
|
sanitized = result.String()
|
||||||
sanitized = strings.TrimSpace(sanitized)
|
sanitized = strings.TrimSpace(sanitized)
|
||||||
|
|
||||||
// Remove leading/trailing dots and spaces (Windows doesn't allow these)
|
|
||||||
sanitized = strings.Trim(sanitized, ". ")
|
sanitized = strings.Trim(sanitized, ". ")
|
||||||
|
|
||||||
// Normalize consecutive spaces to single space
|
|
||||||
re = regexp.MustCompile(`\s+`)
|
re = regexp.MustCompile(`\s+`)
|
||||||
sanitized = re.ReplaceAllString(sanitized, " ")
|
sanitized = re.ReplaceAllString(sanitized, " ")
|
||||||
|
|
||||||
// Normalize consecutive underscores to single underscore
|
|
||||||
re = regexp.MustCompile(`_+`)
|
re = regexp.MustCompile(`_+`)
|
||||||
sanitized = re.ReplaceAllString(sanitized, "_")
|
sanitized = re.ReplaceAllString(sanitized, "_")
|
||||||
|
|
||||||
// Remove leading/trailing underscores and spaces
|
|
||||||
sanitized = strings.Trim(sanitized, "_ ")
|
sanitized = strings.Trim(sanitized, "_ ")
|
||||||
|
|
||||||
if sanitized == "" {
|
if sanitized == "" {
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the result is valid UTF-8
|
|
||||||
if !utf8.ValidString(sanitized) {
|
if !utf8.ValidString(sanitized) {
|
||||||
// If invalid UTF-8, try to fix it
|
|
||||||
sanitized = strings.ToValidUTF8(sanitized, "_")
|
sanitized = strings.ToValidUTF8(sanitized, "_")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
// NormalizePath only normalizes path separators without modifying folder names
|
func GetFirstArtist(artistString string) string {
|
||||||
// Use this for user-provided paths that already exist on the filesystem
|
if artistString == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
delimiters := []string{", ", " & ", " feat. ", " ft. ", " featuring "}
|
||||||
|
for _, d := range delimiters {
|
||||||
|
if idx := strings.Index(strings.ToLower(artistString), d); idx != -1 {
|
||||||
|
return strings.TrimSpace(artistString[:idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return artistString
|
||||||
|
}
|
||||||
|
|
||||||
func NormalizePath(folderPath string) string {
|
func NormalizePath(folderPath string) string {
|
||||||
// Normalize all forward slashes to backslashes on Windows
|
|
||||||
return strings.ReplaceAll(folderPath, "/", string(filepath.Separator))
|
return strings.ReplaceAll(folderPath, "/", string(filepath.Separator))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeFolderPath sanitizes each component of a folder path and normalizes separators
|
func GetSeparator() string {
|
||||||
// Use this only for NEW folders being created (artist names, album names, etc.)
|
settings, err := LoadConfigSettings()
|
||||||
|
if err != nil || settings == nil {
|
||||||
|
return "; "
|
||||||
|
}
|
||||||
|
|
||||||
|
if sep, ok := settings["separator"].(string); ok {
|
||||||
|
if sep == "comma" {
|
||||||
|
return ", "
|
||||||
|
}
|
||||||
|
if sep == "semicolon" {
|
||||||
|
return "; "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "; "
|
||||||
|
}
|
||||||
|
|
||||||
func SanitizeFolderPath(folderPath string) string {
|
func SanitizeFolderPath(folderPath string) string {
|
||||||
// Normalize all forward slashes to backslashes on Windows
|
|
||||||
normalizedPath := strings.ReplaceAll(folderPath, "/", string(filepath.Separator))
|
normalizedPath := strings.ReplaceAll(folderPath, "/", string(filepath.Separator))
|
||||||
|
|
||||||
// Detect separator
|
|
||||||
sep := string(filepath.Separator)
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
// Split path into components
|
|
||||||
parts := strings.Split(normalizedPath, sep)
|
parts := strings.Split(normalizedPath, sep)
|
||||||
sanitizedParts := make([]string, 0, len(parts))
|
sanitizedParts := make([]string, 0, len(parts))
|
||||||
|
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
// Keep drive letter intact on Windows (e.g., "C:")
|
|
||||||
if i == 0 && len(part) == 2 && part[1] == ':' {
|
if i == 0 && len(part) == 2 && part[1] == ':' {
|
||||||
sanitizedParts = append(sanitizedParts, part)
|
sanitizedParts = append(sanitizedParts, part)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep empty first part for absolute paths on Unix (e.g., "/Users/...")
|
|
||||||
if i == 0 && part == "" {
|
if i == 0 && part == "" {
|
||||||
sanitizedParts = append(sanitizedParts, part)
|
sanitizedParts = append(sanitizedParts, part)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize each folder name (but don't replace / or \ since we already normalized)
|
|
||||||
sanitized := sanitizeFolderName(part)
|
sanitized := sanitizeFolderName(part)
|
||||||
if sanitized != "" {
|
if sanitized != "" {
|
||||||
sanitizedParts = append(sanitizedParts, sanitized)
|
sanitizedParts = append(sanitizedParts, sanitized)
|
||||||
@@ -164,8 +225,15 @@ func SanitizeFolderPath(folderPath string) string {
|
|||||||
return strings.Join(sanitizedParts, sep)
|
return strings.Join(sanitizedParts, sep)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeFolderName removes invalid characters from a single folder name
|
func sanitizeFolderName(name string) string { return SanitizeFilename(name) }
|
||||||
func sanitizeFolderName(name string) string {
|
|
||||||
// Use the same sanitization as filename
|
func sanitizeFilename(name string) string {
|
||||||
return sanitizeFilename(name)
|
return SanitizeFilename(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizeOptionalFilename(name string) string {
|
||||||
|
if strings.TrimSpace(name) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return SanitizeFilename(name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func OpenFolderInExplorer(path string) error {
|
|||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "windows":
|
case "windows":
|
||||||
cmd = exec.Command("explorer", path)
|
cmd = exec.Command("explorer", path)
|
||||||
case "darwin": // macOS
|
case "darwin":
|
||||||
cmd = exec.Command("open", path)
|
cmd = exec.Command("open", path)
|
||||||
case "linux":
|
case "linux":
|
||||||
cmd = exec.Command("xdg-open", path)
|
cmd = exec.Command("xdg-open", path)
|
||||||
@@ -26,7 +26,7 @@ func OpenFolderInExplorer(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SelectFolderDialog(ctx context.Context, defaultPath string) (string, error) {
|
func SelectFolderDialog(ctx context.Context, defaultPath string) (string, error) {
|
||||||
// If defaultPath is empty, use default music path
|
|
||||||
if defaultPath == "" {
|
if defaultPath == "" {
|
||||||
defaultPath = GetDefaultMusicPath()
|
defaultPath = GetDefaultMusicPath()
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,6 @@ func SelectFolderDialog(ctx context.Context, defaultPath string) (string, error)
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user cancelled, selectedPath will be empty
|
|
||||||
if selectedPath == "" {
|
if selectedPath == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
@@ -51,12 +50,28 @@ func SelectFolderDialog(ctx context.Context, defaultPath string) (string, error)
|
|||||||
|
|
||||||
func SelectFileDialog(ctx context.Context) (string, error) {
|
func SelectFileDialog(ctx context.Context) (string, error) {
|
||||||
options := wailsRuntime.OpenDialogOptions{
|
options := wailsRuntime.OpenDialogOptions{
|
||||||
Title: "Select FLAC File for Analysis",
|
Title: "Select Audio File for Analysis",
|
||||||
Filters: []wailsRuntime.FileFilter{
|
Filters: []wailsRuntime.FileFilter{
|
||||||
|
{
|
||||||
|
DisplayName: "Audio Files (*.flac;*.mp3;*.m4a;*.aac)",
|
||||||
|
Pattern: "*.flac;*.mp3;*.m4a;*.aac",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
DisplayName: "FLAC Audio Files (*.flac)",
|
DisplayName: "FLAC Audio Files (*.flac)",
|
||||||
Pattern: "*.flac",
|
Pattern: "*.flac",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "MP3 Audio Files (*.mp3)",
|
||||||
|
Pattern: "*.mp3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "M4A Audio Files (*.m4a)",
|
||||||
|
Pattern: "*.m4a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "AAC Audio Files (*.aac)",
|
||||||
|
Pattern: "*.aac",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
DisplayName: "All Files (*.*)",
|
DisplayName: "All Files (*.*)",
|
||||||
Pattern: "*.*",
|
Pattern: "*.*",
|
||||||
@@ -69,10 +84,32 @@ func SelectFileDialog(ctx context.Context) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user cancelled, selectedFile will be empty
|
|
||||||
if selectedFile == "" {
|
if selectedFile == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectedFile, nil
|
return selectedFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SelectImageVideoDialog(ctx context.Context) ([]string, error) {
|
||||||
|
options := wailsRuntime.OpenDialogOptions{
|
||||||
|
Title: "Select Image or Video",
|
||||||
|
Filters: []wailsRuntime.FileFilter{
|
||||||
|
{
|
||||||
|
DisplayName: "Supported Files (*.jpg, *.png, *.mp4, *.mov, ...)",
|
||||||
|
Pattern: "*.jpg;*.jpeg;*.png;*.gif;*.webp;*.mp4;*.mkv;*.webm;*.mov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "All Files (*.*)",
|
||||||
|
Pattern: "*.*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPaths, err := wailsRuntime.OpenMultipleFilesDialog(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedPaths, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HistoryItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SpotifyID string `json:"spotify_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Artists string `json:"artists"`
|
||||||
|
Album string `json:"album"`
|
||||||
|
DurationStr string `json:"duration_str"`
|
||||||
|
CoverURL string `json:"cover_url"`
|
||||||
|
Quality string `json:"quality"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var historyDB *bolt.DB
|
||||||
|
|
||||||
|
const (
|
||||||
|
historyBucket = "DownloadHistory"
|
||||||
|
maxHistory = 10000
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitHistoryDB(appName string) error {
|
||||||
|
|
||||||
|
appDir, err := EnsureAppDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dbPath := filepath.Join(appDir, "history.db")
|
||||||
|
|
||||||
|
db, err := bolt.Open(dbPath, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Update(func(tx *bolt.Tx) error {
|
||||||
|
_, err := tx.CreateBucketIfNotExists([]byte(historyBucket))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
db.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
historyDB = db
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseHistoryDB() {
|
||||||
|
if historyDB != nil {
|
||||||
|
historyDB.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddHistoryItem(item HistoryItem, appName string) error {
|
||||||
|
if historyDB == nil {
|
||||||
|
if err := InitHistoryDB(appName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return historyDB.Update(func(tx *bolt.Tx) error {
|
||||||
|
b, err := tx.CreateBucketIfNotExists([]byte(historyBucket))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id, _ := b.NextSequence()
|
||||||
|
|
||||||
|
item.ID = fmt.Sprintf("%d-%d", time.Now().UnixNano(), id)
|
||||||
|
item.Timestamp = time.Now().Unix()
|
||||||
|
|
||||||
|
buf, err := json.Marshal(item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Stats().KeyN >= maxHistory {
|
||||||
|
c := b.Cursor()
|
||||||
|
|
||||||
|
toDelete := maxHistory / 20
|
||||||
|
if toDelete < 1 {
|
||||||
|
toDelete = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for k, _ := c.First(); k != nil && count < toDelete; k, _ = c.Next() {
|
||||||
|
if err := b.Delete(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Put([]byte(item.ID), buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHistoryItems(appName string) ([]HistoryItem, error) {
|
||||||
|
if historyDB == nil {
|
||||||
|
if err := InitHistoryDB(appName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var items []HistoryItem
|
||||||
|
err := historyDB.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte(historyBucket))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := b.Cursor()
|
||||||
|
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
var item HistoryItem
|
||||||
|
if err := json.Unmarshal(v, &item); err == nil {
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].Timestamp > items[j].Timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearHistory(appName string) error {
|
||||||
|
if historyDB == nil {
|
||||||
|
if err := InitHistoryDB(appName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return historyDB.Update(func(tx *bolt.Tx) error {
|
||||||
|
return tx.DeleteBucket([]byte(historyBucket))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetchHistoryItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Info string `json:"info"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
IsExplicit bool `json:"is_explicit,omitempty"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
fetchHistoryBucket = "FetchHistory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddFetchHistoryItem(item FetchHistoryItem, appName string) error {
|
||||||
|
if historyDB == nil {
|
||||||
|
if err := InitHistoryDB(appName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return historyDB.Update(func(tx *bolt.Tx) error {
|
||||||
|
b, err := tx.CreateBucketIfNotExists([]byte(fetchHistoryBucket))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id, _ := b.NextSequence()
|
||||||
|
|
||||||
|
if item.URL != "" {
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
var existing FetchHistoryItem
|
||||||
|
if err := json.Unmarshal(v, &existing); err == nil {
|
||||||
|
if existing.URL == item.URL && existing.Type == item.Type {
|
||||||
|
if err := b.Delete(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.ID = fmt.Sprintf("%d-%d", time.Now().UnixNano(), id)
|
||||||
|
item.Timestamp = time.Now().Unix()
|
||||||
|
|
||||||
|
buf, err := json.Marshal(item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Stats().KeyN >= maxHistory {
|
||||||
|
c := b.Cursor()
|
||||||
|
toDelete := maxHistory / 20
|
||||||
|
if toDelete < 1 {
|
||||||
|
toDelete = 1
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
for k, _ := c.First(); k != nil && count < toDelete; k, _ = c.Next() {
|
||||||
|
if err := b.Delete(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Put([]byte(item.ID), buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFetchHistoryItems(appName string) ([]FetchHistoryItem, error) {
|
||||||
|
if historyDB == nil {
|
||||||
|
if err := InitHistoryDB(appName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var items []FetchHistoryItem
|
||||||
|
err := historyDB.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte(fetchHistoryBucket))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := b.Cursor()
|
||||||
|
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
var item FetchHistoryItem
|
||||||
|
if err := json.Unmarshal(v, &item); err == nil {
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].Timestamp > items[j].Timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearFetchHistory(appName string) error {
|
||||||
|
if historyDB == nil {
|
||||||
|
if err := InitHistoryDB(appName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return historyDB.Update(func(tx *bolt.Tx) error {
|
||||||
|
return tx.DeleteBucket([]byte(fetchHistoryBucket))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearFetchHistoryByType(itemType string, appName string) error {
|
||||||
|
if historyDB == nil {
|
||||||
|
if err := InitHistoryDB(appName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return historyDB.Update(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte(fetchHistoryBucket))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var keysToDelete [][]byte
|
||||||
|
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
var item FetchHistoryItem
|
||||||
|
if err := json.Unmarshal(v, &item); err == nil {
|
||||||
|
if item.Type == itemType {
|
||||||
|
keysToDelete = append(keysToDelete, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range keysToDelete {
|
||||||
|
if err := b.Delete(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteHistoryItem(id string, appName string) error {
|
||||||
|
if historyDB == nil {
|
||||||
|
if err := InitHistoryDB(appName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return historyDB.Update(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte(historyBucket))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Delete([]byte(id))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFetchHistoryItem(id string, appName string) error {
|
||||||
|
if historyDB == nil {
|
||||||
|
if err := InitHistoryDB(appName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return historyDB.Update(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte(fetchHistoryBucket))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b.Delete([]byte(id))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultDownloaderUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
func NewRequestWithDefaultHeaders(method string, rawURL string, body io.Reader) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequest(method, rawURL, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", DefaultDownloaderUserAgent)
|
||||||
|
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
isrcCacheDBFile = "isrc_cache.db"
|
||||||
|
isrcCacheBucket = "SpotifyTrackISRC"
|
||||||
|
)
|
||||||
|
|
||||||
|
type isrcCacheEntry struct {
|
||||||
|
TrackID string `json:"track_id"`
|
||||||
|
ISRC string `json:"isrc"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
isrcCacheDB *bolt.DB
|
||||||
|
isrcCacheDBMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitISRCCacheDB() error {
|
||||||
|
isrcCacheDBMu.Lock()
|
||||||
|
defer isrcCacheDBMu.Unlock()
|
||||||
|
|
||||||
|
if isrcCacheDB != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appDir, err := EnsureAppDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := filepath.Join(appDir, isrcCacheDBFile)
|
||||||
|
db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
_, err := tx.CreateBucketIfNotExists([]byte(isrcCacheBucket))
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isrcCacheDB = db
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseISRCCacheDB() {
|
||||||
|
isrcCacheDBMu.Lock()
|
||||||
|
defer isrcCacheDBMu.Unlock()
|
||||||
|
|
||||||
|
if isrcCacheDB != nil {
|
||||||
|
_ = isrcCacheDB.Close()
|
||||||
|
isrcCacheDB = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCachedISRC(trackID string) (string, error) {
|
||||||
|
normalizedTrackID := strings.TrimSpace(trackID)
|
||||||
|
if normalizedTrackID == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := InitISRCCacheDB(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachedISRC string
|
||||||
|
err := isrcCacheDB.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(isrcCacheBucket))
|
||||||
|
if bucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value := bucket.Get([]byte(normalizedTrackID))
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry isrcCacheEntry
|
||||||
|
if err := json.Unmarshal(value, &entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedISRC = strings.ToUpper(strings.TrimSpace(entry.ISRC))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedISRC, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutCachedISRC(trackID string, isrc string) error {
|
||||||
|
normalizedTrackID := strings.TrimSpace(trackID)
|
||||||
|
normalizedISRC := strings.ToUpper(strings.TrimSpace(isrc))
|
||||||
|
if normalizedTrackID == "" || normalizedISRC == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := InitISRCCacheDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := isrcCacheEntry{
|
||||||
|
TrackID: normalizedTrackID,
|
||||||
|
ISRC: normalizedISRC,
|
||||||
|
UpdatedAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encode ISRC cache entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return isrcCacheDB.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := tx.CreateBucketIfNotExists([]byte(isrcCacheBucket))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(normalizedTrackID), payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
spotifySessionTokenURL = "https://open.spotify.com/api/token"
|
||||||
|
spotifyGIDMetadataURL = "https://spclient.wg.spotify.com/metadata/4/%s/%s?market=from_token"
|
||||||
|
spotifyBase62Alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
spotifyTokenCacheFile = ".isrc-finder-token.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var spotifyAnonymousTokenMu sync.Mutex
|
||||||
|
|
||||||
|
type spotifyAnonymousToken struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
AccessTokenExpirationTimestampMs int64 `json:"accessTokenExpirationTimestampMs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type spotifyTrackRawData struct {
|
||||||
|
Album struct {
|
||||||
|
GID string `json:"gid"`
|
||||||
|
} `json:"album"`
|
||||||
|
ExternalID []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"external_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type spotifyAlbumRawData struct {
|
||||||
|
ExternalID []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"external_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpotifyTrackIdentifiers struct {
|
||||||
|
ISRC string `json:"isrc,omitempty"`
|
||||||
|
UPC string `json:"upc,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSpotifyTrackIdentifiersDirect(spotifyTrackID string) (SpotifyTrackIdentifiers, error) {
|
||||||
|
normalizedTrackID, err := extractSpotifyTrackID(spotifyTrackID)
|
||||||
|
if err != nil {
|
||||||
|
return SpotifyTrackIdentifiers{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
identifiers := SpotifyTrackIdentifiers{}
|
||||||
|
|
||||||
|
cachedISRC, err := GetCachedISRC(normalizedTrackID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: failed to read ISRC cache: %v\n", err)
|
||||||
|
} else if cachedISRC != "" {
|
||||||
|
fmt.Printf("Found ISRC in cache: %s\n", cachedISRC)
|
||||||
|
identifiers.ISRC = cachedISRC
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
|
payload, metadataErr := fetchSpotifyTrackRawData(httpClient, normalizedTrackID)
|
||||||
|
if metadataErr == nil {
|
||||||
|
metadataIdentifiers, extractErr := extractSpotifyTrackIdentifiers(httpClient, payload)
|
||||||
|
if extractErr == nil {
|
||||||
|
mergeSpotifyTrackIdentifiers(&identifiers, metadataIdentifiers)
|
||||||
|
if identifiers.ISRC != "" {
|
||||||
|
fmt.Printf("Found identifiers via Spotify metadata: isrc=%s upc=%s\n", identifiers.ISRC, identifiers.UPC)
|
||||||
|
cacheResolvedSpotifyTrackISRC(normalizedTrackID, "", identifiers.ISRC)
|
||||||
|
}
|
||||||
|
if identifiers.ISRC != "" && identifiers.UPC != "" {
|
||||||
|
return identifiers, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadataErr = extractErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadataErr != nil {
|
||||||
|
fmt.Printf("Warning: Spotify metadata identifier lookup failed, falling back to Soundplate: %v\n", metadataErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if identifiers.ISRC == "" {
|
||||||
|
client := NewSongLinkClient()
|
||||||
|
isrc, resolvedTrackID, soundplateErr := client.lookupSpotifyISRCViaSoundplate(normalizedTrackID)
|
||||||
|
if soundplateErr == nil && isrc != "" {
|
||||||
|
identifiers.ISRC = isrc
|
||||||
|
fmt.Printf("Found ISRC via Soundplate: %s\n", isrc)
|
||||||
|
cacheResolvedSpotifyTrackISRC(normalizedTrackID, resolvedTrackID, isrc)
|
||||||
|
return identifiers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadataErr != nil && soundplateErr != nil {
|
||||||
|
return identifiers, fmt.Errorf("spotify metadata lookup failed: %v | soundplate lookup failed: %w", metadataErr, soundplateErr)
|
||||||
|
}
|
||||||
|
if soundplateErr != nil && identifiers.UPC == "" {
|
||||||
|
return identifiers, soundplateErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if identifiers.ISRC != "" || identifiers.UPC != "" {
|
||||||
|
return identifiers, nil
|
||||||
|
}
|
||||||
|
if metadataErr != nil {
|
||||||
|
return identifiers, metadataErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return identifiers, fmt.Errorf("no Spotify identifiers found for track %s", normalizedTrackID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) lookupSpotifyISRC(spotifyTrackID string) (string, error) {
|
||||||
|
identifiers, err := GetSpotifyTrackIdentifiersDirect(spotifyTrackID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if identifiers.ISRC == "" {
|
||||||
|
return "", fmt.Errorf("no Spotify ISRC found for track %s", strings.TrimSpace(spotifyTrackID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return identifiers.ISRC, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheResolvedSpotifyTrackISRC(trackID string, resolvedTrackID string, isrc string) {
|
||||||
|
if err := PutCachedISRC(trackID, isrc); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to write ISRC cache: %v\n", err)
|
||||||
|
}
|
||||||
|
if resolvedTrackID != "" && resolvedTrackID != trackID {
|
||||||
|
if err := PutCachedISRC(resolvedTrackID, isrc); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to write ISRC cache for resolved track ID: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeSpotifyTrackIdentifiers(target *SpotifyTrackIdentifiers, incoming SpotifyTrackIdentifiers) {
|
||||||
|
if incoming.ISRC != "" {
|
||||||
|
target.ISRC = strings.TrimSpace(incoming.ISRC)
|
||||||
|
}
|
||||||
|
if incoming.UPC != "" {
|
||||||
|
target.UPC = strings.TrimSpace(incoming.UPC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupSpotifyAlbumUPC(albumID string) (string, error) {
|
||||||
|
normalizedAlbumID := strings.TrimSpace(albumID)
|
||||||
|
if normalizedAlbumID == "" {
|
||||||
|
return "", fmt.Errorf("spotify album ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
payload, err := fetchSpotifyAlbumRawData(httpClient, normalizedAlbumID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractSpotifyAlbumUPC(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestSpotifyBytes(client *http.Client, targetURL string, headers map[string]string) ([]byte, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, targetURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||||
|
details := strings.TrimSpace(string(body))
|
||||||
|
if details == "" {
|
||||||
|
details = resp.Status
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("request failed: %s", details)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestSpotifyJSON(client *http.Client, targetURL string, headers map[string]string, target interface{}) error {
|
||||||
|
body, err := requestSpotifyBytes(client, targetURL, headers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, target); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse JSON response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSpotifyCachedToken() (*spotifyAnonymousToken, error) {
|
||||||
|
cachePath, err := spotifyTokenCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := os.ReadFile(cachePath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read token cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var token spotifyAnonymousToken
|
||||||
|
if err := json.Unmarshal(body, &token); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read token cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveSpotifyCachedToken(token *spotifyAnonymousToken) error {
|
||||||
|
cachePath, err := spotifyTokenCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(cachePath), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create token cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.MarshalIndent(token, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(cachePath, body, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write token cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func spotifyTokenCachePath() (string, error) {
|
||||||
|
appDir, err := EnsureAppDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(appDir, spotifyTokenCacheFile), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func spotifyTokenIsValid(token *spotifyAnonymousToken) bool {
|
||||||
|
if token == nil || token.AccessToken == "" || token.AccessTokenExpirationTimestampMs == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now().UnixMilli() < token.AccessTokenExpirationTimestampMs-30_000
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestSpotifyAnonymousAccessToken(client *http.Client) (string, error) {
|
||||||
|
spotifyAnonymousTokenMu.Lock()
|
||||||
|
defer spotifyAnonymousTokenMu.Unlock()
|
||||||
|
|
||||||
|
cachedToken, err := loadSpotifyCachedToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if spotifyTokenIsValid(cachedToken) {
|
||||||
|
return cachedToken.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedTOTP, version, err := generateSpotifyTOTP(time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate Spotify TOTP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := url.Values{
|
||||||
|
"reason": {"init"},
|
||||||
|
"productType": {"web-player"},
|
||||||
|
"totp": {generatedTOTP},
|
||||||
|
"totpServer": {generatedTOTP},
|
||||||
|
"totpVer": {strconv.Itoa(version)},
|
||||||
|
}
|
||||||
|
|
||||||
|
var token spotifyAnonymousToken
|
||||||
|
if err := requestSpotifyJSON(client, spotifySessionTokenURL+"?"+query.Encode(), nil, &token); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := saveSpotifyCachedToken(&token); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSpotifyTrackID(value string) (string, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return "", errors.New("track input is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(value, "spotify:track:") {
|
||||||
|
return value[strings.LastIndex(value, ":")+1:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := url.Parse(value)
|
||||||
|
if err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") {
|
||||||
|
parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
|
||||||
|
if len(parts) >= 2 && parts[0] == "track" {
|
||||||
|
return parts[1], nil
|
||||||
|
}
|
||||||
|
return "", errors.New("expected URL like https://open.spotify.com/track/<id>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(value) == 22 {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("track must be a Spotify track ID, URL, or URI")
|
||||||
|
}
|
||||||
|
|
||||||
|
func spotifyTrackIDToGID(trackID string) (string, error) {
|
||||||
|
return spotifyEntityIDToGID(trackID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func spotifyEntityIDToGID(entityID string) (string, error) {
|
||||||
|
if entityID == "" {
|
||||||
|
return "", errors.New("entity ID is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
value := big.NewInt(0)
|
||||||
|
base := big.NewInt(62)
|
||||||
|
|
||||||
|
for _, char := range entityID {
|
||||||
|
index := strings.IndexRune(spotifyBase62Alphabet, char)
|
||||||
|
if index < 0 {
|
||||||
|
return "", fmt.Errorf("invalid base62 character: %q", string(char))
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Mul(value, base)
|
||||||
|
value.Add(value, big.NewInt(int64(index)))
|
||||||
|
}
|
||||||
|
|
||||||
|
hexValue := value.Text(16)
|
||||||
|
if len(hexValue) < 32 {
|
||||||
|
hexValue = strings.Repeat("0", 32-len(hexValue)) + hexValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSpotifyTrackRawData(client *http.Client, trackID string) ([]byte, error) {
|
||||||
|
gid, err := spotifyTrackIDToGID(trackID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchSpotifyRawMetadataByGID(client, "track", gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSpotifyAlbumRawData(client *http.Client, albumID string) ([]byte, error) {
|
||||||
|
gid, err := spotifyEntityIDToGID(albumID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchSpotifyRawMetadataByGID(client, "album", gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSpotifyRawMetadataByGID(client *http.Client, entityType string, gid string) ([]byte, error) {
|
||||||
|
accessToken, err := requestSpotifyAnonymousAccessToken(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestSpotifyBytes(
|
||||||
|
client,
|
||||||
|
fmt.Sprintf(spotifyGIDMetadataURL, entityType, gid),
|
||||||
|
map[string]string{
|
||||||
|
"authorization": "Bearer " + accessToken,
|
||||||
|
"accept": "application/json",
|
||||||
|
"user-agent": songLinkUserAgent,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSpotifyTrackIdentifiers(client *http.Client, payload []byte) (SpotifyTrackIdentifiers, error) {
|
||||||
|
var track spotifyTrackRawData
|
||||||
|
if err := json.Unmarshal(payload, &track); err != nil {
|
||||||
|
return SpotifyTrackIdentifiers{}, fmt.Errorf("failed to decode Spotify track metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
identifiers := SpotifyTrackIdentifiers{}
|
||||||
|
for _, externalID := range track.ExternalID {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(externalID.Type), "isrc") {
|
||||||
|
if isrc := firstISRCMatch(externalID.ID); isrc != "" {
|
||||||
|
identifiers.ISRC = isrc
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if identifiers.ISRC == "" {
|
||||||
|
identifiers.ISRC = firstISRCMatch(string(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
albumGID := strings.TrimSpace(track.Album.GID)
|
||||||
|
if client != nil && albumGID != "" {
|
||||||
|
albumPayload, err := fetchSpotifyRawMetadataByGID(client, "album", albumGID)
|
||||||
|
if err == nil {
|
||||||
|
if upc, upcErr := extractSpotifyAlbumUPC(albumPayload); upcErr == nil {
|
||||||
|
identifiers.UPC = upc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return identifiers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSpotifyTrackISRC(payload []byte) (string, error) {
|
||||||
|
identifiers, err := extractSpotifyTrackIdentifiers(nil, payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if identifiers.ISRC != "" {
|
||||||
|
return identifiers.ISRC, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("ISRC not found in Spotify track metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSpotifyAlbumUPC(payload []byte) (string, error) {
|
||||||
|
var album spotifyAlbumRawData
|
||||||
|
if err := json.Unmarshal(payload, &album); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode Spotify album metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, externalID := range album.ExternalID {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(externalID.Type), "upc") {
|
||||||
|
upc := strings.TrimSpace(externalID.ID)
|
||||||
|
if upc != "" {
|
||||||
|
return upc, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("UPC not found in Spotify album metadata")
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func ResolveTrackISRC(spotifyTrackID string) string {
|
||||||
|
spotifyTrackID = strings.TrimSpace(spotifyTrackID)
|
||||||
|
if spotifyTrackID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if cachedISRC, err := GetCachedISRC(spotifyTrackID); err == nil && cachedISRC != "" {
|
||||||
|
return strings.ToUpper(strings.TrimSpace(cachedISRC))
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewSongLinkClient()
|
||||||
|
isrc, err := client.GetISRCDirect(spotifyTrackID)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(strings.TrimSpace(isrc))
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type resolvedTrackLinks struct {
|
||||||
|
TidalURL string
|
||||||
|
AmazonURL string
|
||||||
|
DeezerURL string
|
||||||
|
ISRC string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
linkResolverProviderSongstats = "songstats"
|
||||||
|
linkResolverProviderDeezerSongLink = "deezer-songlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *SongLinkClient) resolveSpotifyTrackLinks(spotifyTrackID string, region string) (*resolvedTrackLinks, error) {
|
||||||
|
links := &resolvedTrackLinks{}
|
||||||
|
var attempts []string
|
||||||
|
|
||||||
|
isrc, err := s.lookupSpotifyISRC(spotifyTrackID)
|
||||||
|
if err != nil {
|
||||||
|
attempts = append(attempts, fmt.Sprintf("spotify isrc: %v", err))
|
||||||
|
} else {
|
||||||
|
links.ISRC = isrc
|
||||||
|
}
|
||||||
|
|
||||||
|
if links.ISRC != "" {
|
||||||
|
resolvers := orderedLinkResolvers()
|
||||||
|
|
||||||
|
for _, resolver := range resolvers {
|
||||||
|
switch resolver {
|
||||||
|
case linkResolverProviderSongstats:
|
||||||
|
addedData, songstatsErr := s.resolveLinksViaSongstats(links)
|
||||||
|
if songstatsErr != nil {
|
||||||
|
attempts = append(attempts, fmt.Sprintf("songstats: %v", songstatsErr))
|
||||||
|
} else if addedData {
|
||||||
|
fmt.Println("Using Songstats as configured link resolver")
|
||||||
|
}
|
||||||
|
case linkResolverProviderDeezerSongLink:
|
||||||
|
addedData, deezerSongLinkErr := s.resolveLinksViaDeezerSongLink(links, region)
|
||||||
|
if deezerSongLinkErr != nil {
|
||||||
|
attempts = append(attempts, fmt.Sprintf("deezer-songlink: %v", deezerSongLinkErr))
|
||||||
|
} else if addedData {
|
||||||
|
fmt.Println("Using Songlink as configured link resolver")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if links.TidalURL != "" && links.AmazonURL != "" {
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAnySongLinkData(links) {
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(attempts) == 0 {
|
||||||
|
attempts = append(attempts, "no streaming URLs found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return links, errors.New(strings.Join(attempts, " | "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func orderedLinkResolvers() []string {
|
||||||
|
preferred := GetLinkResolverSetting()
|
||||||
|
if !GetLinkResolverAllowFallback() {
|
||||||
|
if preferred == linkResolverProviderDeezerSongLink {
|
||||||
|
return []string{linkResolverProviderDeezerSongLink}
|
||||||
|
}
|
||||||
|
return []string{linkResolverProviderSongstats}
|
||||||
|
}
|
||||||
|
|
||||||
|
if preferred == linkResolverProviderDeezerSongLink {
|
||||||
|
return []string{
|
||||||
|
linkResolverProviderDeezerSongLink,
|
||||||
|
linkResolverProviderSongstats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{
|
||||||
|
linkResolverProviderSongstats,
|
||||||
|
linkResolverProviderDeezerSongLink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) resolveLinksViaSongstats(links *resolvedTrackLinks) (bool, error) {
|
||||||
|
if links == nil || links.ISRC == "" {
|
||||||
|
return false, fmt.Errorf("ISRC is required for Songstats resolver")
|
||||||
|
}
|
||||||
|
|
||||||
|
before := *links
|
||||||
|
|
||||||
|
fmt.Printf("Fetching Songstats links for ISRC %s\n", links.ISRC)
|
||||||
|
if err := s.populateLinksFromSongstats(links, links.ISRC); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return *links != before, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) resolveLinksViaDeezerSongLink(links *resolvedTrackLinks, region string) (bool, error) {
|
||||||
|
if links == nil || links.ISRC == "" {
|
||||||
|
return false, fmt.Errorf("ISRC is required for Deezer song.link resolver")
|
||||||
|
}
|
||||||
|
|
||||||
|
before := *links
|
||||||
|
var attempts []string
|
||||||
|
|
||||||
|
if links.DeezerURL == "" {
|
||||||
|
fmt.Printf("Resolving Deezer track from ISRC %s\n", links.ISRC)
|
||||||
|
deezerURL, err := s.lookupDeezerTrackURLByISRC(links.ISRC)
|
||||||
|
if err != nil {
|
||||||
|
attempts = append(attempts, fmt.Sprintf("deezer isrc: %v", err))
|
||||||
|
} else {
|
||||||
|
links.DeezerURL = deezerURL
|
||||||
|
fmt.Printf("Found Deezer URL: %s\n", links.DeezerURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if links.DeezerURL != "" {
|
||||||
|
fmt.Println("Resolving streaming URLs from song.link via Deezer URL...")
|
||||||
|
deezerResp, err := s.fetchSongLinkLinksByURL(links.DeezerURL, region)
|
||||||
|
if err != nil {
|
||||||
|
attempts = append(attempts, fmt.Sprintf("song.link deezer: %v", err))
|
||||||
|
} else {
|
||||||
|
mergeSongLinkResponse(links, deezerResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if links.ISRC == "" {
|
||||||
|
if resolvedISRC, deezerISRCErr := getDeezerISRC(links.DeezerURL); deezerISRCErr == nil {
|
||||||
|
links.ISRC = resolvedISRC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *links != before {
|
||||||
|
if len(attempts) == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return true, errors.New(strings.Join(attempts, " | "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(attempts) == 0 {
|
||||||
|
attempts = append(attempts, "no links found via deezer-songlink")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, errors.New(strings.Join(attempts, " | "))
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -14,7 +13,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LRCLibResponse represents the LRCLIB API response
|
|
||||||
type LRCLibResponse struct {
|
type LRCLibResponse struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -27,33 +25,34 @@ type LRCLibResponse struct {
|
|||||||
SyncedLyrics string `json:"syncedLyrics"`
|
SyncedLyrics string `json:"syncedLyrics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LyricsLine represents a single line of lyrics
|
|
||||||
type LyricsLine struct {
|
type LyricsLine struct {
|
||||||
StartTimeMs string `json:"startTimeMs"`
|
StartTimeMs string `json:"startTimeMs"`
|
||||||
Words string `json:"words"`
|
Words string `json:"words"`
|
||||||
EndTimeMs string `json:"endTimeMs"`
|
EndTimeMs string `json:"endTimeMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LyricsResponse represents the API response
|
|
||||||
type LyricsResponse struct {
|
type LyricsResponse struct {
|
||||||
Error bool `json:"error"`
|
Error bool `json:"error"`
|
||||||
SyncType string `json:"syncType"`
|
SyncType string `json:"syncType"`
|
||||||
Lines []LyricsLine `json:"lines"`
|
Lines []LyricsLine `json:"lines"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LyricsDownloadRequest represents a request to download lyrics
|
|
||||||
type LyricsDownloadRequest struct {
|
type LyricsDownloadRequest struct {
|
||||||
SpotifyID string `json:"spotify_id"`
|
SpotifyID string `json:"spotify_id"`
|
||||||
TrackName string `json:"track_name"`
|
TrackName string `json:"track_name"`
|
||||||
ArtistName string `json:"artist_name"`
|
ArtistName string `json:"artist_name"`
|
||||||
|
AlbumName string `json:"album_name"`
|
||||||
|
AlbumArtist string `json:"album_artist"`
|
||||||
|
ReleaseDate string `json:"release_date"`
|
||||||
|
ISRC string `json:"isrc"`
|
||||||
OutputDir string `json:"output_dir"`
|
OutputDir string `json:"output_dir"`
|
||||||
FilenameFormat string `json:"filename_format"`
|
FilenameFormat string `json:"filename_format"`
|
||||||
TrackNumber bool `json:"track_number"`
|
TrackNumber bool `json:"track_number"`
|
||||||
Position int `json:"position"`
|
Position int `json:"position"`
|
||||||
UseAlbumTrackNumber bool `json:"use_album_track_number"`
|
UseAlbumTrackNumber bool `json:"use_album_track_number"`
|
||||||
|
DiscNumber int `json:"disc_number"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LyricsDownloadResponse represents the response from lyrics download
|
|
||||||
type LyricsDownloadResponse struct {
|
type LyricsDownloadResponse struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -62,27 +61,30 @@ type LyricsDownloadResponse struct {
|
|||||||
AlreadyExists bool `json:"already_exists,omitempty"`
|
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LyricsClient handles lyrics fetching
|
|
||||||
type LyricsClient struct {
|
type LyricsClient struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLyricsClient creates a new lyrics client
|
|
||||||
func NewLyricsClient() *LyricsClient {
|
func NewLyricsClient() *LyricsClient {
|
||||||
return &LyricsClient{
|
return &LyricsClient{
|
||||||
httpClient: &http.Client{Timeout: 15 * time.Second},
|
httpClient: &http.Client{Timeout: 15 * time.Second},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchLyricsWithMetadata fetches lyrics using track name and artist from LRCLIB
|
func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName, albumName string, duration int) (*LyricsResponse, error) {
|
||||||
func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName string) (*LyricsResponse, error) {
|
|
||||||
// Try LRCLIB API
|
apiURL := fmt.Sprintf("https://lrclib.net/api/get?artist_name=%s&track_name=%s",
|
||||||
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9scmNsaWIubmV0L2FwaS9nZXQ/YXJ0aXN0X25hbWU9")
|
|
||||||
apiURL := fmt.Sprintf("%s%s&track_name=%s",
|
|
||||||
string(apiBase),
|
|
||||||
url.QueryEscape(artistName),
|
url.QueryEscape(artistName),
|
||||||
url.QueryEscape(trackName))
|
url.QueryEscape(trackName))
|
||||||
|
|
||||||
|
if albumName != "" {
|
||||||
|
apiURL = fmt.Sprintf("%s&album_name=%s", apiURL, url.QueryEscape(albumName))
|
||||||
|
}
|
||||||
|
|
||||||
|
if duration > 0 {
|
||||||
|
apiURL = fmt.Sprintf("%s&duration=%d", apiURL, duration)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := c.httpClient.Get(apiURL)
|
resp, err := c.httpClient.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch from LRCLIB: %v", err)
|
return nil, fmt.Errorf("failed to fetch from LRCLIB: %v", err)
|
||||||
@@ -103,11 +105,13 @@ func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName string) (*L
|
|||||||
return nil, fmt.Errorf("failed to parse LRCLIB response: %v", err)
|
return nil, fmt.Errorf("failed to parse LRCLIB response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert LRCLIB response to our LyricsResponse format
|
if lrcLibResp.SyncedLyrics == "" && lrcLibResp.PlainLyrics == "" {
|
||||||
|
return nil, fmt.Errorf("LRCLIB returned empty lyrics")
|
||||||
|
}
|
||||||
|
|
||||||
return c.convertLRCLibToLyricsResponse(&lrcLibResp), nil
|
return c.convertLRCLibToLyricsResponse(&lrcLibResp), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertLRCLibToLyricsResponse converts LRCLIB response to our standard format
|
|
||||||
func (c *LyricsClient) convertLRCLibToLyricsResponse(lrcLib *LRCLibResponse) *LyricsResponse {
|
func (c *LyricsClient) convertLRCLibToLyricsResponse(lrcLib *LRCLibResponse) *LyricsResponse {
|
||||||
resp := &LyricsResponse{
|
resp := &LyricsResponse{
|
||||||
Error: false,
|
Error: false,
|
||||||
@@ -115,7 +119,6 @@ func (c *LyricsClient) convertLRCLibToLyricsResponse(lrcLib *LRCLibResponse) *Ly
|
|||||||
Lines: []LyricsLine{},
|
Lines: []LyricsLine{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer synced lyrics, fall back to plain
|
|
||||||
lyricsText := lrcLib.SyncedLyrics
|
lyricsText := lrcLib.SyncedLyrics
|
||||||
if lyricsText == "" {
|
if lyricsText == "" {
|
||||||
lyricsText = lrcLib.PlainLyrics
|
lyricsText = lrcLib.PlainLyrics
|
||||||
@@ -127,7 +130,6 @@ func (c *LyricsClient) convertLRCLibToLyricsResponse(lrcLib *LRCLibResponse) *Ly
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse synced lyrics format [mm:ss.xx] text
|
|
||||||
lines := strings.Split(lyricsText, "\n")
|
lines := strings.Split(lyricsText, "\n")
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
@@ -135,14 +137,12 @@ func (c *LyricsClient) convertLRCLibToLyricsResponse(lrcLib *LRCLibResponse) *Ly
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if line has timestamp [mm:ss.xx]
|
|
||||||
if strings.HasPrefix(line, "[") && len(line) > 10 {
|
if strings.HasPrefix(line, "[") && len(line) > 10 {
|
||||||
closeBracket := strings.Index(line, "]")
|
closeBracket := strings.Index(line, "]")
|
||||||
if closeBracket > 0 {
|
if closeBracket > 0 {
|
||||||
timestamp := line[1:closeBracket]
|
timestamp := line[1:closeBracket]
|
||||||
words := strings.TrimSpace(line[closeBracket+1:])
|
words := strings.TrimSpace(line[closeBracket+1:])
|
||||||
|
|
||||||
// Convert [mm:ss.xx] to milliseconds
|
|
||||||
ms := lrcTimestampToMs(timestamp)
|
ms := lrcTimestampToMs(timestamp)
|
||||||
resp.Lines = append(resp.Lines, LyricsLine{
|
resp.Lines = append(resp.Lines, LyricsLine{
|
||||||
StartTimeMs: fmt.Sprintf("%d", ms),
|
StartTimeMs: fmt.Sprintf("%d", ms),
|
||||||
@@ -152,9 +152,8 @@ func (c *LyricsClient) convertLRCLibToLyricsResponse(lrcLib *LRCLibResponse) *Ly
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plain lyrics line (no timestamp)
|
|
||||||
resp.Lines = append(resp.Lines, LyricsLine{
|
resp.Lines = append(resp.Lines, LyricsLine{
|
||||||
StartTimeMs: "0",
|
StartTimeMs: "",
|
||||||
Words: line,
|
Words: line,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -162,10 +161,9 @@ func (c *LyricsClient) convertLRCLibToLyricsResponse(lrcLib *LRCLibResponse) *Ly
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// lrcTimestampToMs converts LRC timestamp [mm:ss.xx] to milliseconds
|
|
||||||
func lrcTimestampToMs(timestamp string) int64 {
|
func lrcTimestampToMs(timestamp string) int64 {
|
||||||
var minutes, seconds, centiseconds int64
|
var minutes, seconds, centiseconds int64
|
||||||
// Try parsing mm:ss.xx format
|
|
||||||
n, _ := fmt.Sscanf(timestamp, "%d:%d.%d", &minutes, &seconds, ¢iseconds)
|
n, _ := fmt.Sscanf(timestamp, "%d:%d.%d", &minutes, &seconds, ¢iseconds)
|
||||||
if n >= 2 {
|
if n >= 2 {
|
||||||
return minutes*60*1000 + seconds*1000 + centiseconds*10
|
return minutes*60*1000 + seconds*1000 + centiseconds*10
|
||||||
@@ -173,11 +171,11 @@ func lrcTimestampToMs(timestamp string) int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchLyricsFromLRCLibSearch fetches lyrics using LRCLIB search API
|
|
||||||
func (c *LyricsClient) FetchLyricsFromLRCLibSearch(trackName, artistName string) (*LyricsResponse, error) {
|
func (c *LyricsClient) FetchLyricsFromLRCLibSearch(trackName, artistName string) (*LyricsResponse, error) {
|
||||||
query := fmt.Sprintf("%s %s", artistName, trackName)
|
|
||||||
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9scmNsaWIubmV0L2FwaS9zZWFyY2g/cT0=")
|
apiURL := fmt.Sprintf("https://lrclib.net/api/search?artist_name=%s&track_name=%s",
|
||||||
apiURL := fmt.Sprintf("%s%s", string(apiBase), url.QueryEscape(query))
|
url.QueryEscape(artistName),
|
||||||
|
url.QueryEscape(trackName))
|
||||||
|
|
||||||
resp, err := c.httpClient.Get(apiURL)
|
resp, err := c.httpClient.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -203,98 +201,157 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(trackName, artistName string)
|
|||||||
return nil, fmt.Errorf("no results found")
|
return nil, fmt.Errorf("no results found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find best match - prefer one with synced lyrics
|
var bestSynced *LRCLibResponse
|
||||||
var best *LRCLibResponse
|
var bestPlain *LRCLibResponse
|
||||||
for i := range results {
|
for i := range results {
|
||||||
if results[i].SyncedLyrics != "" {
|
if results[i].SyncedLyrics != "" && bestSynced == nil {
|
||||||
best = &results[i]
|
bestSynced = &results[i]
|
||||||
break
|
|
||||||
}
|
}
|
||||||
if best == nil && results[i].PlainLyrics != "" {
|
if results[i].PlainLyrics != "" && bestPlain == nil {
|
||||||
best = &results[i]
|
bestPlain = &results[i]
|
||||||
|
}
|
||||||
|
if bestSynced != nil {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
best := bestSynced
|
||||||
|
if best == nil {
|
||||||
|
best = bestPlain
|
||||||
|
}
|
||||||
if best == nil {
|
if best == nil {
|
||||||
best = &results[0]
|
best = &results[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if best.SyncedLyrics == "" && best.PlainLyrics == "" {
|
||||||
|
return nil, fmt.Errorf("no lyrics found in search results")
|
||||||
|
}
|
||||||
|
|
||||||
return c.convertLRCLibToLyricsResponse(best), nil
|
return c.convertLRCLibToLyricsResponse(best), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// simplifyTrackName removes common suffixes like "(feat. X)", "(Remastered)", etc.
|
|
||||||
func simplifyTrackName(name string) string {
|
func simplifyTrackName(name string) string {
|
||||||
// Remove content in parentheses
|
|
||||||
if idx := strings.Index(name, "("); idx > 0 {
|
if idx := strings.Index(name, "("); idx > 0 {
|
||||||
name = strings.TrimSpace(name[:idx])
|
name = strings.TrimSpace(name[:idx])
|
||||||
}
|
}
|
||||||
// Remove content after " - " (like "From the Motion Picture")
|
|
||||||
if idx := strings.Index(name, " - "); idx > 0 {
|
if idx := strings.Index(name, " - "); idx > 0 {
|
||||||
name = strings.TrimSpace(name[:idx])
|
name = strings.TrimSpace(name[:idx])
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchLyricsAllSources tries all LRCLIB sources to get lyrics
|
func isSynced(resp *LyricsResponse) bool {
|
||||||
func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string) (*LyricsResponse, string, error) {
|
return resp != nil && !resp.Error && resp.SyncType == "LINE_SYNCED" && len(resp.Lines) > 0
|
||||||
// 1. Try LRCLIB exact match
|
}
|
||||||
resp, err := c.FetchLyricsWithMetadata(trackName, artistName)
|
|
||||||
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
|
||||||
return resp, "LRCLIB", nil
|
|
||||||
}
|
|
||||||
fmt.Printf(" LRCLIB exact: %v\n", err)
|
|
||||||
|
|
||||||
// 2. Try LRCLIB search
|
func hasLyrics(resp *LyricsResponse) bool {
|
||||||
resp, err = c.FetchLyricsFromLRCLibSearch(trackName, artistName)
|
return resp != nil && !resp.Error && len(resp.Lines) > 0
|
||||||
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
}
|
||||||
return resp, "LRCLIB Search", nil
|
|
||||||
}
|
func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName, albumName string, duration int) (*LyricsResponse, string, error) {
|
||||||
fmt.Printf(" LRCLIB search: %v\n", err)
|
|
||||||
|
var unsyncedFallback *LyricsResponse
|
||||||
|
var unsyncedSource string
|
||||||
|
|
||||||
|
check := func(resp *LyricsResponse, err error, source string) (*LyricsResponse, string, bool) {
|
||||||
|
if err != nil || resp == nil || resp.Error || len(resp.Lines) == 0 {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
if isSynced(resp) {
|
||||||
|
return resp, source, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsyncedFallback == nil {
|
||||||
|
unsyncedFallback = resp
|
||||||
|
unsyncedSource = source
|
||||||
|
}
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *LyricsResponse
|
||||||
|
var src string
|
||||||
|
var found bool
|
||||||
|
|
||||||
|
resp, _ = c.FetchLyricsWithMetadata(trackName, artistName, albumName, duration)
|
||||||
|
resp, src, found = check(resp, nil, "LRCLIB")
|
||||||
|
if found {
|
||||||
|
fmt.Printf(" [LRCLIB] Synced found via exact match (with album)\n")
|
||||||
|
return resp, src, nil
|
||||||
|
}
|
||||||
|
fmt.Printf(" LRCLIB exact (with album): no synced\n")
|
||||||
|
|
||||||
|
if albumName != "" {
|
||||||
|
resp, _ = c.FetchLyricsWithMetadata(trackName, artistName, "", duration)
|
||||||
|
resp, src, found = check(resp, nil, "LRCLIB (no album)")
|
||||||
|
if found {
|
||||||
|
fmt.Printf(" [LRCLIB] Synced found via exact match (no album)\n")
|
||||||
|
return resp, src, nil
|
||||||
|
}
|
||||||
|
fmt.Printf(" LRCLIB exact (no album): no synced\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, _ = c.FetchLyricsFromLRCLibSearch(trackName, artistName)
|
||||||
|
resp, src, found = check(resp, nil, "LRCLIB Search")
|
||||||
|
if found {
|
||||||
|
fmt.Printf(" [LRCLIB] Synced found via search\n")
|
||||||
|
return resp, src, nil
|
||||||
|
}
|
||||||
|
fmt.Printf(" LRCLIB search: no synced\n")
|
||||||
|
|
||||||
// 3. Try with simplified track name (remove parentheses, subtitles)
|
|
||||||
simplifiedTrack := simplifyTrackName(trackName)
|
simplifiedTrack := simplifyTrackName(trackName)
|
||||||
if simplifiedTrack != trackName {
|
if simplifiedTrack != trackName {
|
||||||
fmt.Printf(" Trying simplified name: %s\n", simplifiedTrack)
|
fmt.Printf(" Trying simplified name: %s\n", simplifiedTrack)
|
||||||
|
|
||||||
resp, err = c.FetchLyricsWithMetadata(simplifiedTrack, artistName)
|
resp, _ = c.FetchLyricsWithMetadata(simplifiedTrack, artistName, albumName, duration)
|
||||||
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
resp, src, found = check(resp, nil, "LRCLIB (simplified)")
|
||||||
return resp, "LRCLIB (simplified)", nil
|
if found {
|
||||||
|
fmt.Printf(" [LRCLIB] Synced found via simplified exact\n")
|
||||||
|
return resp, src, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = c.FetchLyricsFromLRCLibSearch(simplifiedTrack, artistName)
|
resp, _ = c.FetchLyricsFromLRCLibSearch(simplifiedTrack, artistName)
|
||||||
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
resp, src, found = check(resp, nil, "LRCLIB Search (simplified)")
|
||||||
return resp, "LRCLIB Search (simplified)", nil
|
if found {
|
||||||
|
fmt.Printf(" [LRCLIB] Synced found via simplified search\n")
|
||||||
|
return resp, src, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if unsyncedFallback != nil {
|
||||||
|
fmt.Printf(" [LRCLIB] No synced found, using unsynced from: %s\n", unsyncedSource)
|
||||||
|
return unsyncedFallback, unsyncedSource + " (unsynced)", nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil, "", fmt.Errorf("lyrics not found in any source")
|
return nil, "", fmt.Errorf("lyrics not found in any source")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertToLRC converts lyrics response to LRC format
|
|
||||||
func (c *LyricsClient) ConvertToLRC(lyrics *LyricsResponse, trackName, artistName string) string {
|
func (c *LyricsClient) ConvertToLRC(lyrics *LyricsResponse, trackName, artistName string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
// Add metadata
|
|
||||||
sb.WriteString(fmt.Sprintf("[ti:%s]\n", trackName))
|
sb.WriteString(fmt.Sprintf("[ti:%s]\n", trackName))
|
||||||
sb.WriteString(fmt.Sprintf("[ar:%s]\n", artistName))
|
sb.WriteString(fmt.Sprintf("[ar:%s]\n", artistName))
|
||||||
sb.WriteString("[by:SpotiFlac]\n")
|
sb.WriteString("[by:SpotiFlac]\n")
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
// Add lyrics lines
|
|
||||||
for _, line := range lyrics.Lines {
|
for _, line := range lyrics.Lines {
|
||||||
if line.Words == "" {
|
if line.Words == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert milliseconds to LRC timestamp format [mm:ss.xx]
|
if line.StartTimeMs == "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s\n", line.Words))
|
||||||
|
} else {
|
||||||
|
|
||||||
timestamp := msToLRCTimestamp(line.StartTimeMs)
|
timestamp := msToLRCTimestamp(line.StartTimeMs)
|
||||||
sb.WriteString(fmt.Sprintf("%s%s\n", timestamp, line.Words))
|
sb.WriteString(fmt.Sprintf("%s%s\n", timestamp, line.Words))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// msToLRCTimestamp converts milliseconds string to LRC timestamp format [mm:ss.xx]
|
|
||||||
func msToLRCTimestamp(msStr string) string {
|
func msToLRCTimestamp(msStr string) string {
|
||||||
var ms int64
|
var ms int64
|
||||||
fmt.Sscanf(msStr, "%d", &ms)
|
fmt.Sscanf(msStr, "%d", &ms)
|
||||||
@@ -307,40 +364,55 @@ func msToLRCTimestamp(msStr string) string {
|
|||||||
return fmt.Sprintf("[%02d:%02d.%02d]", minutes, seconds, centiseconds)
|
return fmt.Sprintf("[%02d:%02d.%02d]", minutes, seconds, centiseconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildLyricsFilename builds the lyrics filename based on settings (same as track filename)
|
func buildLyricsFilename(trackName, artistName, albumName, albumArtist, releaseDate, filenameFormat, isrc string, includeTrackNumber bool, position, discNumber int) string {
|
||||||
func buildLyricsFilename(trackName, artistName, filenameFormat string, includeTrackNumber bool, position int) string {
|
|
||||||
safeTitle := sanitizeFilename(trackName)
|
safeTitle := sanitizeFilename(trackName)
|
||||||
safeArtist := sanitizeFilename(artistName)
|
safeArtist := sanitizeFilename(artistName)
|
||||||
|
safeAlbum := sanitizeFilename(albumName)
|
||||||
|
safeAlbumArtist := sanitizeFilename(albumArtist)
|
||||||
|
safeISRC := SanitizeOptionalFilename(isrc)
|
||||||
|
|
||||||
|
year := ""
|
||||||
|
if len(releaseDate) >= 4 {
|
||||||
|
year = releaseDate[:4]
|
||||||
|
}
|
||||||
|
|
||||||
var filename string
|
var filename string
|
||||||
|
|
||||||
// Check if format is a template (contains {})
|
|
||||||
if strings.Contains(filenameFormat, "{") {
|
if strings.Contains(filenameFormat, "{") {
|
||||||
filename = filenameFormat
|
filename = filenameFormat
|
||||||
filename = strings.ReplaceAll(filename, "{title}", safeTitle)
|
filename = strings.ReplaceAll(filename, "{title}", safeTitle)
|
||||||
filename = strings.ReplaceAll(filename, "{artist}", safeArtist)
|
filename = strings.ReplaceAll(filename, "{artist}", safeArtist)
|
||||||
|
filename = strings.ReplaceAll(filename, "{album}", safeAlbum)
|
||||||
|
filename = strings.ReplaceAll(filename, "{album_artist}", safeAlbumArtist)
|
||||||
|
filename = strings.ReplaceAll(filename, "{year}", year)
|
||||||
|
filename = strings.ReplaceAll(filename, "{date}", sanitizeFilename(releaseDate))
|
||||||
|
filename = strings.ReplaceAll(filename, "{isrc}", safeISRC)
|
||||||
|
|
||||||
|
if discNumber > 0 {
|
||||||
|
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))
|
||||||
|
} else {
|
||||||
|
filename = strings.ReplaceAll(filename, "{disc}", "")
|
||||||
|
}
|
||||||
|
|
||||||
// Handle track number - if position is 0, remove {track} and surrounding separators
|
|
||||||
if position > 0 {
|
if position > 0 {
|
||||||
filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", position))
|
filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", position))
|
||||||
} else {
|
} else {
|
||||||
// Remove {track} with common separators
|
|
||||||
filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "")
|
filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "")
|
||||||
filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "")
|
filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "")
|
||||||
filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "")
|
filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy format support
|
|
||||||
switch filenameFormat {
|
switch filenameFormat {
|
||||||
case "artist-title":
|
case "artist-title":
|
||||||
filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle)
|
filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle)
|
||||||
case "title":
|
case "title":
|
||||||
filename = safeTitle
|
filename = safeTitle
|
||||||
default: // "title-artist"
|
default:
|
||||||
filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add track number prefix if enabled (legacy behavior)
|
|
||||||
if includeTrackNumber && position > 0 {
|
if includeTrackNumber && position > 0 {
|
||||||
filename = fmt.Sprintf("%02d. %s", position, filename)
|
filename = fmt.Sprintf("%02d. %s", position, filename)
|
||||||
}
|
}
|
||||||
@@ -349,7 +421,47 @@ func buildLyricsFilename(trackName, artistName, filenameFormat string, includeTr
|
|||||||
return filename + ".lrc"
|
return filename + ".lrc"
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadLyrics downloads lyrics for a single track
|
func findAudioFileForLyrics(dir, trackName, artistName string) string {
|
||||||
|
|
||||||
|
safeTitle := sanitizeFilename(trackName)
|
||||||
|
safeArtist := sanitizeFilename(artistName)
|
||||||
|
|
||||||
|
audioExts := []string{".flac", ".mp3", ".m4a", ".FLAC", ".MP3", ".M4A"}
|
||||||
|
|
||||||
|
patterns := []string{
|
||||||
|
fmt.Sprintf("%s - %s", safeTitle, safeArtist),
|
||||||
|
fmt.Sprintf("%s - %s", safeArtist, safeTitle),
|
||||||
|
safeTitle,
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := entry.Name()
|
||||||
|
baseName := strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
if strings.HasPrefix(baseName, pattern) || strings.Contains(baseName, pattern) {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
for _, audioExt := range audioExts {
|
||||||
|
if ext == strings.ToLower(audioExt) {
|
||||||
|
return filepath.Join(dir, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloadResponse, error) {
|
func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloadResponse, error) {
|
||||||
if req.SpotifyID == "" {
|
if req.SpotifyID == "" {
|
||||||
return &LyricsDownloadResponse{
|
return &LyricsDownloadResponse{
|
||||||
@@ -358,7 +470,6 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa
|
|||||||
}, fmt.Errorf("spotify ID is required")
|
}, fmt.Errorf("spotify ID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create output directory if it doesn't exist
|
|
||||||
outputDir := req.OutputDir
|
outputDir := req.OutputDir
|
||||||
if outputDir == "" {
|
if outputDir == "" {
|
||||||
outputDir = GetDefaultMusicPath()
|
outputDir = GetDefaultMusicPath()
|
||||||
@@ -373,16 +484,19 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa
|
|||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate filename using same format as track
|
|
||||||
filenameFormat := req.FilenameFormat
|
filenameFormat := req.FilenameFormat
|
||||||
if filenameFormat == "" {
|
if filenameFormat == "" {
|
||||||
filenameFormat = "title-artist" // default
|
filenameFormat = "title-artist"
|
||||||
}
|
}
|
||||||
filename := buildLyricsFilename(req.TrackName, req.ArtistName, filenameFormat, req.TrackNumber, req.Position)
|
resolvedISRC := strings.TrimSpace(req.ISRC)
|
||||||
|
if resolvedISRC == "" && strings.Contains(filenameFormat, "{isrc}") {
|
||||||
|
resolvedISRC = ResolveTrackISRC(req.SpotifyID)
|
||||||
|
}
|
||||||
|
filename := buildLyricsFilename(req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, filenameFormat, resolvedISRC, req.TrackNumber, req.Position, req.DiscNumber)
|
||||||
filePath := filepath.Join(outputDir, filename)
|
filePath := filepath.Join(outputDir, filename)
|
||||||
|
|
||||||
// Check if file already exists
|
filePath, alreadyExists := ResolveOutputPathForDownload(filePath, GetRedownloadWithSuffixSetting())
|
||||||
if fileInfo, err := os.Stat(filePath); err == nil && fileInfo.Size() > 0 {
|
if alreadyExists {
|
||||||
return &LyricsDownloadResponse{
|
return &LyricsDownloadResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
Message: "Lyrics file already exists",
|
Message: "Lyrics file already exists",
|
||||||
@@ -391,8 +505,17 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch lyrics from LRCLIB
|
audioDuration := 0
|
||||||
lyrics, _, err := c.FetchLyricsAllSources(req.SpotifyID, req.TrackName, req.ArtistName)
|
audioFile := findAudioFileForLyrics(outputDir, req.TrackName, req.ArtistName)
|
||||||
|
if audioFile != "" {
|
||||||
|
duration, err := GetAudioDuration(audioFile)
|
||||||
|
if err == nil && duration > 0 {
|
||||||
|
audioDuration = int(duration)
|
||||||
|
fmt.Printf("[DownloadLyrics] Found audio file, duration: %d seconds\n", audioDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lyrics, _, err := c.FetchLyricsAllSources(req.SpotifyID, req.TrackName, req.ArtistName, req.AlbumName, audioDuration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &LyricsDownloadResponse{
|
return &LyricsDownloadResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -400,10 +523,8 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa
|
|||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to LRC format
|
|
||||||
lrcContent := c.ConvertToLRC(lyrics, req.TrackName, req.ArtistName)
|
lrcContent := c.ConvertToLRC(lyrics, req.TrackName, req.ArtistName)
|
||||||
|
|
||||||
// Write LRC file
|
|
||||||
if err := os.WriteFile(filePath, []byte(lrcContent), 0644); err != nil {
|
if err := os.WriteFile(filePath, []byte(lrcContent), 0644); err != nil {
|
||||||
return &LyricsDownloadResponse{
|
return &LyricsDownloadResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
id3v2 "github.com/bogem/id3v2/v2"
|
||||||
|
"github.com/go-flac/flacvorbis"
|
||||||
|
"github.com/go-flac/go-flac"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmbeddedLyrics struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Lyrics string `json:"lyrics"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Synced bool `json:"synced"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var lrcTimestampRe = regexp.MustCompile(`\[\d{1,2}:\d{2}(?:\.\d{1,3})?\]`)
|
||||||
|
|
||||||
|
func isSyncedLyrics(lyrics string) bool {
|
||||||
|
return lrcTimestampRe.MatchString(lyrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadEmbeddedLyrics(filePath string) (*EmbeddedLyrics, error) {
|
||||||
|
if !fileExists(filePath) {
|
||||||
|
return nil, fmt.Errorf("file does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &EmbeddedLyrics{
|
||||||
|
Path: filePath,
|
||||||
|
Name: filepath.Base(filePath),
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
|
||||||
|
var lyrics string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch ext {
|
||||||
|
case ".lrc", ".txt":
|
||||||
|
var content []byte
|
||||||
|
content, err = os.ReadFile(filePath)
|
||||||
|
if err == nil {
|
||||||
|
lyrics = string(content)
|
||||||
|
result.Source = "lrc"
|
||||||
|
}
|
||||||
|
case ".flac":
|
||||||
|
lyrics, err = readFlacLyrics(filePath)
|
||||||
|
result.Source = "embedded"
|
||||||
|
case ".mp3":
|
||||||
|
lyrics, err = readMp3Lyrics(filePath)
|
||||||
|
result.Source = "embedded"
|
||||||
|
case ".m4a", ".aac", ".opus", ".ogg":
|
||||||
|
lyrics, err = readLyricsWithFFprobe(filePath)
|
||||||
|
result.Source = "embedded"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported file format: %s", ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lyrics = strings.TrimSpace(lyrics)
|
||||||
|
if lyrics == "" {
|
||||||
|
result.Error = "No lyrics found in this file"
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Lyrics = lyrics
|
||||||
|
result.Synced = isSyncedLyrics(lyrics)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFlacLyrics(filePath string) (string, error) {
|
||||||
|
f, err := flac.ParseFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse FLAC file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, block := range f.Meta {
|
||||||
|
if block.Type != flac.VorbisComment {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmt, err := flacvorbis.ParseFromMetaDataBlock(*block)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, comment := range cmt.Comments {
|
||||||
|
parts := strings.SplitN(comment, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldName := strings.ToUpper(parts[0])
|
||||||
|
switch fieldName {
|
||||||
|
case "LYRICS", "UNSYNCEDLYRICS", "SYNCEDLYRICS", "LYRICS-XXX":
|
||||||
|
if strings.TrimSpace(parts[1]) != "" {
|
||||||
|
return parts[1], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMp3Lyrics(filePath string) (string, error) {
|
||||||
|
tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to open MP3 file: %w", err)
|
||||||
|
}
|
||||||
|
defer tag.Close()
|
||||||
|
|
||||||
|
frames := tag.GetFrames(tag.CommonID("Unsynchronised lyrics/text transcription"))
|
||||||
|
for _, frame := range frames {
|
||||||
|
uslf, ok := frame.(id3v2.UnsynchronisedLyricsFrame)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(uslf.Lyrics) != "" {
|
||||||
|
return uslf.Lyrics, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLyricsWithFFprobe(filePath string) (string, error) {
|
||||||
|
ffprobePath, err := GetFFprobePath()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateExecutable(ffprobePath); err != nil {
|
||||||
|
return "", fmt.Errorf("invalid ffprobe executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(ffprobePath,
|
||||||
|
"-v", "quiet",
|
||||||
|
"-print_format", "json",
|
||||||
|
"-show_format",
|
||||||
|
"-show_streams",
|
||||||
|
filePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
setHideWindow(cmd)
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var probe struct {
|
||||||
|
Format struct {
|
||||||
|
Tags map[string]string `json:"tags"`
|
||||||
|
} `json:"format"`
|
||||||
|
Streams []struct {
|
||||||
|
Tags map[string]string `json:"tags"`
|
||||||
|
} `json:"streams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(output, &probe); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
collect := func(tags map[string]string) string {
|
||||||
|
for key, value := range tags {
|
||||||
|
lk := strings.ToLower(key)
|
||||||
|
if lk == "lyrics" || strings.HasPrefix(lk, "lyrics-") || lk == "unsyncedlyrics" {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if lyrics := collect(probe.Format.Tags); lyrics != "" {
|
||||||
|
return lyrics, nil
|
||||||
|
}
|
||||||
|
for _, stream := range probe.Streams {
|
||||||
|
if lyrics := collect(stream.Tags); lyrics != "" {
|
||||||
|
return lyrics, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtractLyricsResult struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
OutputPath string `json:"output_path,omitempty"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractLyricsToLRC(filePath string, overwrite bool) (*ExtractLyricsResult, error) {
|
||||||
|
result := &ExtractLyricsResult{Path: filePath}
|
||||||
|
|
||||||
|
embedded, err := ReadEmbeddedLyrics(filePath)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if embedded.Error != "" {
|
||||||
|
result.Error = embedded.Error
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(embedded.Lyrics) == "" {
|
||||||
|
result.Error = "No lyrics found in this file"
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(filePath)
|
||||||
|
base := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
|
||||||
|
outputPath := filepath.Join(dir, base+".lrc")
|
||||||
|
result.OutputPath = outputPath
|
||||||
|
|
||||||
|
if !overwrite {
|
||||||
|
if info, statErr := os.Stat(outputPath); statErr == nil && info.Size() > 0 {
|
||||||
|
result.AlreadyExists = true
|
||||||
|
result.Error = "LRC file already exists"
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content := embedded.Lyrics
|
||||||
|
if !strings.HasSuffix(content, "\n") {
|
||||||
|
content += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if writeErr := os.WriteFile(outputPath, []byte(content), 0644); writeErr != nil {
|
||||||
|
result.Error = fmt.Sprintf("failed to write LRC file: %v", writeErr)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SelectLyricsFiles(ctx context.Context) ([]string, error) {
|
||||||
|
return runtime.OpenMultipleFilesDialog(ctx, runtime.OpenDialogOptions{
|
||||||
|
Title: "Select Lyrics or Audio Files",
|
||||||
|
Filters: []runtime.FileFilter{
|
||||||
|
{
|
||||||
|
DisplayName: "Lyrics & Audio (*.lrc, *.flac, *.mp3, *.m4a, *.opus)",
|
||||||
|
Pattern: "*.lrc;*.flac;*.mp3;*.m4a;*.aac;*.opus;*.ogg;*.txt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "LRC Files (*.lrc)",
|
||||||
|
Pattern: "*.lrc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "Audio Files (*.flac, *.mp3, *.m4a, *.opus)",
|
||||||
|
Pattern: "*.flac;*.mp3;*.m4a;*.aac;*.opus;*.ogg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "All Files (*.*)",
|
||||||
|
Pattern: "*.*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Eyevinn/mp4ff/mp4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func decryptWithMP4FF(keySpecs []string, inputPath, outputPath string) error {
|
||||||
|
key, keysByKID, strictKIDMode, err := parseMP4FFKeySpecs(keySpecs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
inFile, err := os.Open(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open encrypted MP4: %w", err)
|
||||||
|
}
|
||||||
|
defer inFile.Close()
|
||||||
|
|
||||||
|
outFile, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create decrypted MP4: %w", err)
|
||||||
|
}
|
||||||
|
outClosed := false
|
||||||
|
defer func() {
|
||||||
|
if !outClosed {
|
||||||
|
_ = outFile.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := decryptMP4FFFileWithKeyMap(inFile, nil, outFile, key, keysByKID, strictKIDMode); err != nil {
|
||||||
|
_ = outFile.Close()
|
||||||
|
outClosed = true
|
||||||
|
_ = os.Remove(outputPath)
|
||||||
|
return fmt.Errorf("mp4ff decryption failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := outFile.Close(); err != nil {
|
||||||
|
outClosed = true
|
||||||
|
_ = os.Remove(outputPath)
|
||||||
|
return fmt.Errorf("failed to finalize decrypted MP4: %w", err)
|
||||||
|
}
|
||||||
|
outClosed = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMP4FFKeySpecs(keySpecs []string) (key []byte, keysByKID map[string][]byte, strictKIDMode bool, err error) {
|
||||||
|
normalizedSpecs := make([]string, 0, len(keySpecs))
|
||||||
|
seenSpecs := make(map[string]struct{}, len(keySpecs))
|
||||||
|
for _, spec := range keySpecs {
|
||||||
|
normalized, err := normalizeMP4FFKeySpec(spec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, err
|
||||||
|
}
|
||||||
|
if normalized == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seenSpecs[normalized]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenSpecs[normalized] = struct{}{}
|
||||||
|
normalizedSpecs = append(normalizedSpecs, normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(normalizedSpecs) == 0 {
|
||||||
|
return nil, nil, false, fmt.Errorf("no mp4ff key specs provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasKIDPair := false
|
||||||
|
hasLegacyKey := false
|
||||||
|
for _, spec := range normalizedSpecs {
|
||||||
|
if strings.Contains(spec, ":") {
|
||||||
|
hasKIDPair = true
|
||||||
|
} else {
|
||||||
|
hasLegacyKey = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasKIDPair && hasLegacyKey {
|
||||||
|
return nil, nil, false, fmt.Errorf("cannot mix legacy key and kid:key key format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasKIDPair {
|
||||||
|
if len(normalizedSpecs) != 1 {
|
||||||
|
return nil, nil, false, fmt.Errorf("multiple legacy keys are not supported")
|
||||||
|
}
|
||||||
|
key, err = mp4.UnpackKey(normalizedSpecs[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, fmt.Errorf("unpacking key: %w", err)
|
||||||
|
}
|
||||||
|
return key, nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keysByKID = make(map[string][]byte, len(normalizedSpecs))
|
||||||
|
for _, spec := range normalizedSpecs {
|
||||||
|
parts := strings.SplitN(spec, ":", 2)
|
||||||
|
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
|
||||||
|
return nil, nil, false, fmt.Errorf("bad kid:key format %q", spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
kid, err := mp4.UnpackKey(strings.TrimSpace(parts[0]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, fmt.Errorf("unpacking kid: %w", err)
|
||||||
|
}
|
||||||
|
kidHex := hex.EncodeToString(kid)
|
||||||
|
if _, exists := keysByKID[kidHex]; exists {
|
||||||
|
return nil, nil, false, fmt.Errorf("duplicate kid %s", kidHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedKey, err := mp4.UnpackKey(strings.TrimSpace(parts[1]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, fmt.Errorf("unpacking key for kid %s: %w", kidHex, err)
|
||||||
|
}
|
||||||
|
keysByKID[kidHex] = parsedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, keysByKID, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeMP4FFKeySpec(spec string) (string, error) {
|
||||||
|
spec = strings.TrimSpace(spec)
|
||||||
|
if spec == "" || !strings.Contains(spec, ":") {
|
||||||
|
return spec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(spec, ":", 2)
|
||||||
|
left := strings.TrimSpace(parts[0])
|
||||||
|
right := strings.TrimSpace(parts[1])
|
||||||
|
if left == "" || right == "" {
|
||||||
|
return "", fmt.Errorf("bad key spec %q", spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := mp4.UnpackKey(left); err == nil {
|
||||||
|
return left + ":" + right, nil
|
||||||
|
}
|
||||||
|
if !isDecimalString(left) {
|
||||||
|
return "", fmt.Errorf("bad kid in key spec %q", spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := mp4.UnpackKey(right); err != nil {
|
||||||
|
return "", fmt.Errorf("bad key spec %q: %w", spec, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return right, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDecimalString(value string) bool {
|
||||||
|
if value == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, ch := range value {
|
||||||
|
if ch < '0' || ch > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptMP4FFFileWithKeyMap(r, initR io.Reader, w io.Writer, key []byte, keysByKID map[string][]byte, strictKIDMode bool) error {
|
||||||
|
inMp4, err := mp4.DecodeFile(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !inMp4.IsFragmented() {
|
||||||
|
return fmt.Errorf("file not fragmented. Not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
init := inMp4.Init
|
||||||
|
if inMp4.Init == nil {
|
||||||
|
if initR == nil {
|
||||||
|
return fmt.Errorf("no init segment file and no init part of file")
|
||||||
|
}
|
||||||
|
initSegment, err := mp4.DecodeFile(initR)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not decode init file: %w", err)
|
||||||
|
}
|
||||||
|
init = initSegment.Init
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptInfo, err := mp4.DecryptInit(init)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if inMp4.Init != nil {
|
||||||
|
if err := inMp4.Init.Encode(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, segment := range inMp4.Segments {
|
||||||
|
if inMp4.Init == nil {
|
||||||
|
if err := segment.ParseSenc(init); err != nil {
|
||||||
|
return fmt.Errorf("parseSenc: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decryptMP4FFSegmentWithSparseSenc(segment, decryptInfo, key, keysByKID, strictKIDMode); err != nil {
|
||||||
|
return fmt.Errorf("decryptSegment: %w", err)
|
||||||
|
}
|
||||||
|
if err := segment.Encode(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptMP4FFSegmentWithSparseSenc(segment *mp4.MediaSegment, decryptInfo mp4.DecryptInfo, key []byte, keysByKID map[string][]byte, strictKIDMode bool) error {
|
||||||
|
for _, fragment := range segment.Fragments {
|
||||||
|
if !mp4FragmentContainsSenc(fragment) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := mp4.DecryptFragmentWithKeys(fragment, decryptInfo, key, keysByKID, strictKIDMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(segment.Sidxs) > 0 {
|
||||||
|
segment.Sidx = nil
|
||||||
|
segment.Sidxs = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mp4FragmentContainsSenc(fragment *mp4.Fragment) bool {
|
||||||
|
if fragment == nil || fragment.Moof == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, traf := range fragment.Moof.Trafs {
|
||||||
|
if traf == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hasSenc, _ := traf.ContainsSencBox()
|
||||||
|
if hasSenc {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppVersion = "Unknown"
|
||||||
|
|
||||||
|
const (
|
||||||
|
musicBrainzAPIBase = "https://musicbrainz.org/ws/2"
|
||||||
|
musicBrainzRequestTimeout = 10 * time.Second
|
||||||
|
musicBrainzRequestRetries = 3
|
||||||
|
musicBrainzRequestRetryWait = 3 * time.Second
|
||||||
|
musicBrainzMinRequestInterval = 1100 * time.Millisecond
|
||||||
|
musicBrainzThrottleCooldownOn503 = 5 * time.Second
|
||||||
|
musicBrainzStatusCheckSkipWindow = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type musicBrainzStatusError struct {
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *musicBrainzStatusError) Error() string {
|
||||||
|
return fmt.Sprintf("MusicBrainz API returned status: %d", e.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
type musicBrainzInflightCall struct {
|
||||||
|
done chan struct{}
|
||||||
|
result Metadata
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
musicBrainzCache sync.Map
|
||||||
|
musicBrainzInflightMu sync.Mutex
|
||||||
|
musicBrainzInflight = make(map[string]*musicBrainzInflightCall)
|
||||||
|
|
||||||
|
musicBrainzThrottleMu sync.Mutex
|
||||||
|
musicBrainzNextRequest time.Time
|
||||||
|
musicBrainzBlockedTill time.Time
|
||||||
|
|
||||||
|
musicBrainzStatusMu sync.RWMutex
|
||||||
|
musicBrainzLastCheckedAt time.Time
|
||||||
|
musicBrainzLastCheckedOnline bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetMusicBrainzStatusCheckResult(online bool) {
|
||||||
|
musicBrainzStatusMu.Lock()
|
||||||
|
defer musicBrainzStatusMu.Unlock()
|
||||||
|
|
||||||
|
musicBrainzLastCheckedAt = time.Now()
|
||||||
|
musicBrainzLastCheckedOnline = online
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShouldSkipMusicBrainzMetadataFetch() bool {
|
||||||
|
musicBrainzStatusMu.RLock()
|
||||||
|
defer musicBrainzStatusMu.RUnlock()
|
||||||
|
|
||||||
|
if musicBrainzLastCheckedAt.IsZero() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if musicBrainzLastCheckedOnline {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Since(musicBrainzLastCheckedAt) <= musicBrainzStatusCheckSkipWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
type MusicBrainzRecordingResponse struct {
|
||||||
|
Recordings []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Length int `json:"length"`
|
||||||
|
Releases []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ReleaseGroup struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
PrimaryType string `json:"primary-type"`
|
||||||
|
} `json:"release-group"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Media []struct {
|
||||||
|
Format string `json:"format"`
|
||||||
|
} `json:"media"`
|
||||||
|
LabelInfo []struct {
|
||||||
|
Label struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"label"`
|
||||||
|
} `json:"label-info"`
|
||||||
|
} `json:"releases"`
|
||||||
|
ArtistCredit []struct {
|
||||||
|
Artist struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"artist"`
|
||||||
|
} `json:"artist-credit"`
|
||||||
|
Tags []struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"tags"`
|
||||||
|
} `json:"recordings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func musicBrainzCacheKey(isrc string, useSingleGenre bool) string {
|
||||||
|
separator := strings.TrimSpace(GetSeparator())
|
||||||
|
if separator == "" {
|
||||||
|
separator = ";"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(strings.TrimSpace(isrc)) + "|" + fmt.Sprintf("%t", useSingleGenre) + "|" + separator
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForMusicBrainzRequestSlot() {
|
||||||
|
musicBrainzThrottleMu.Lock()
|
||||||
|
|
||||||
|
readyAt := musicBrainzNextRequest
|
||||||
|
if musicBrainzBlockedTill.After(readyAt) {
|
||||||
|
readyAt = musicBrainzBlockedTill
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if readyAt.Before(now) {
|
||||||
|
readyAt = now
|
||||||
|
}
|
||||||
|
|
||||||
|
musicBrainzNextRequest = readyAt.Add(musicBrainzMinRequestInterval)
|
||||||
|
waitDuration := time.Until(readyAt)
|
||||||
|
|
||||||
|
musicBrainzThrottleMu.Unlock()
|
||||||
|
|
||||||
|
if waitDuration > 0 {
|
||||||
|
time.Sleep(waitDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func noteMusicBrainzThrottle() {
|
||||||
|
musicBrainzThrottleMu.Lock()
|
||||||
|
defer musicBrainzThrottleMu.Unlock()
|
||||||
|
|
||||||
|
cooldownUntil := time.Now().Add(musicBrainzThrottleCooldownOn503)
|
||||||
|
if cooldownUntil.After(musicBrainzBlockedTill) {
|
||||||
|
musicBrainzBlockedTill = cooldownUntil
|
||||||
|
}
|
||||||
|
if musicBrainzNextRequest.Before(musicBrainzBlockedTill) {
|
||||||
|
musicBrainzNextRequest = musicBrainzBlockedTill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRetryMusicBrainzRequest(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
statusErr, ok := err.(*musicBrainzStatusError)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusErr.StatusCode == http.StatusServiceUnavailable || statusErr.StatusCode >= http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryMusicBrainzRecordings(client *http.Client, query string) (*MusicBrainzRecordingResponse, error) {
|
||||||
|
reqURL := fmt.Sprintf("%s/recording?query=%s&fmt=json&inc=releases+artist-credits+tags+media+release-groups+labels", musicBrainzAPIBase, url.QueryEscape(query))
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", fmt.Sprintf("SpotiFLAC/%s ( support@spotbye.qzz.io )", AppVersion))
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < musicBrainzRequestRetries; attempt++ {
|
||||||
|
waitForMusicBrainzRequestSlot()
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err == nil && resp != nil && resp.StatusCode == http.StatusOK {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var mbResp MusicBrainzRecordingResponse
|
||||||
|
if decodeErr := json.NewDecoder(resp.Body).Decode(&mbResp); decodeErr != nil {
|
||||||
|
return nil, decodeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mbResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
} else if resp == nil {
|
||||||
|
lastErr = fmt.Errorf("empty response from MusicBrainz")
|
||||||
|
} else {
|
||||||
|
if resp.StatusCode == http.StatusServiceUnavailable {
|
||||||
|
noteMusicBrainzThrottle()
|
||||||
|
}
|
||||||
|
lastErr = &musicBrainzStatusError{StatusCode: resp.StatusCode}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt < musicBrainzRequestRetries-1 && shouldRetryMusicBrainzRequest(lastErr) {
|
||||||
|
time.Sleep(musicBrainzRequestRetryWait)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr == nil {
|
||||||
|
lastErr = fmt.Errorf("empty response from MusicBrainz")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchMusicBrainzMetadata(isrc, title, artist, album string, useSingleGenre bool, embedGenre bool) (Metadata, error) {
|
||||||
|
var meta Metadata
|
||||||
|
var resultErr error
|
||||||
|
|
||||||
|
if !embedGenre {
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isrc == "" {
|
||||||
|
resultErr = fmt.Errorf("no ISRC provided")
|
||||||
|
return meta, resultErr
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := musicBrainzCacheKey(isrc, useSingleGenre)
|
||||||
|
if cached, ok := musicBrainzCache.Load(cacheKey); ok {
|
||||||
|
return cached.(Metadata), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ShouldSkipMusicBrainzMetadataFetch() {
|
||||||
|
resultErr = fmt.Errorf("skipping MusicBrainz lookup because the latest status check reported offline")
|
||||||
|
return meta, resultErr
|
||||||
|
}
|
||||||
|
|
||||||
|
musicBrainzInflightMu.Lock()
|
||||||
|
if call, ok := musicBrainzInflight[cacheKey]; ok {
|
||||||
|
musicBrainzInflightMu.Unlock()
|
||||||
|
<-call.done
|
||||||
|
return call.result, call.err
|
||||||
|
}
|
||||||
|
|
||||||
|
call := &musicBrainzInflightCall{done: make(chan struct{})}
|
||||||
|
musicBrainzInflight[cacheKey] = call
|
||||||
|
musicBrainzInflightMu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
call.result = meta
|
||||||
|
call.err = resultErr
|
||||||
|
|
||||||
|
musicBrainzInflightMu.Lock()
|
||||||
|
delete(musicBrainzInflight, cacheKey)
|
||||||
|
close(call.done)
|
||||||
|
musicBrainzInflightMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: musicBrainzRequestTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf("isrc:%s", isrc)
|
||||||
|
mbResp, err := queryMusicBrainzRecordings(client, query)
|
||||||
|
if err != nil {
|
||||||
|
resultErr = err
|
||||||
|
return meta, resultErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mbResp.Recordings) == 0 {
|
||||||
|
resultErr = fmt.Errorf("no recordings found for ISRC: %s", isrc)
|
||||||
|
return meta, resultErr
|
||||||
|
}
|
||||||
|
|
||||||
|
recording := mbResp.Recordings[0]
|
||||||
|
|
||||||
|
var genres []string
|
||||||
|
caser := cases.Title(language.English)
|
||||||
|
|
||||||
|
if useSingleGenre {
|
||||||
|
|
||||||
|
maxCount := -1
|
||||||
|
var bestTag string
|
||||||
|
|
||||||
|
for _, tag := range recording.Tags {
|
||||||
|
if tag.Count > maxCount {
|
||||||
|
maxCount = tag.Count
|
||||||
|
bestTag = tag.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestTag != "" {
|
||||||
|
meta.Genre = caser.String(bestTag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, tag := range recording.Tags {
|
||||||
|
|
||||||
|
genres = append(genres, caser.String(tag.Name))
|
||||||
|
}
|
||||||
|
if len(genres) > 0 {
|
||||||
|
|
||||||
|
if len(genres) > 5 {
|
||||||
|
genres = genres[:5]
|
||||||
|
}
|
||||||
|
meta.Genre = strings.Join(genres, GetSeparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.Genre == "" {
|
||||||
|
resultErr = fmt.Errorf("no genre tags found in MusicBrainz")
|
||||||
|
return meta, resultErr
|
||||||
|
}
|
||||||
|
|
||||||
|
musicBrainzCache.Store(cacheKey, meta)
|
||||||
|
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DownloadStatus represents the status of a download item
|
|
||||||
type DownloadStatus string
|
type DownloadStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -18,24 +17,22 @@ const (
|
|||||||
StatusSkipped DownloadStatus = "skipped"
|
StatusSkipped DownloadStatus = "skipped"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DownloadItem represents a single item in the download queue
|
|
||||||
type DownloadItem struct {
|
type DownloadItem struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
TrackName string `json:"track_name"`
|
TrackName string `json:"track_name"`
|
||||||
ArtistName string `json:"artist_name"`
|
ArtistName string `json:"artist_name"`
|
||||||
AlbumName string `json:"album_name"`
|
AlbumName string `json:"album_name"`
|
||||||
ISRC string `json:"isrc"`
|
SpotifyID string `json:"spotify_id"`
|
||||||
Status DownloadStatus `json:"status"`
|
Status DownloadStatus `json:"status"`
|
||||||
Progress float64 `json:"progress"` // MB downloaded
|
Progress float64 `json:"progress"`
|
||||||
TotalSize float64 `json:"total_size"` // MB total (if known)
|
TotalSize float64 `json:"total_size"`
|
||||||
Speed float64 `json:"speed"` // MB/s
|
Speed float64 `json:"speed"`
|
||||||
StartTime int64 `json:"start_time"` // Unix timestamp
|
StartTime int64 `json:"start_time"`
|
||||||
EndTime int64 `json:"end_time"` // Unix timestamp
|
EndTime int64 `json:"end_time"`
|
||||||
ErrorMessage string `json:"error_message"` // If failed
|
ErrorMessage string `json:"error_message"`
|
||||||
FilePath string `json:"file_path"` // Final file path
|
FilePath string `json:"file_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global progress tracker
|
|
||||||
var (
|
var (
|
||||||
currentProgress float64
|
currentProgress float64
|
||||||
currentProgressLock sync.RWMutex
|
currentProgressLock sync.RWMutex
|
||||||
@@ -44,7 +41,9 @@ var (
|
|||||||
currentSpeed float64
|
currentSpeed float64
|
||||||
speedLock sync.RWMutex
|
speedLock sync.RWMutex
|
||||||
|
|
||||||
// Download queue tracking
|
rateLimitUntilMs int64
|
||||||
|
rateLimitLock sync.RWMutex
|
||||||
|
|
||||||
downloadQueue []DownloadItem
|
downloadQueue []DownloadItem
|
||||||
downloadQueueLock sync.RWMutex
|
downloadQueueLock sync.RWMutex
|
||||||
currentItemID string
|
currentItemID string
|
||||||
@@ -55,27 +54,26 @@ var (
|
|||||||
sessionStartLock sync.RWMutex
|
sessionStartLock sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProgressInfo represents download progress information
|
|
||||||
type ProgressInfo struct {
|
type ProgressInfo struct {
|
||||||
IsDownloading bool `json:"is_downloading"`
|
IsDownloading bool `json:"is_downloading"`
|
||||||
MBDownloaded float64 `json:"mb_downloaded"`
|
MBDownloaded float64 `json:"mb_downloaded"`
|
||||||
SpeedMBps float64 `json:"speed_mbps"`
|
SpeedMBps float64 `json:"speed_mbps"`
|
||||||
|
RateLimited bool `json:"rate_limited"`
|
||||||
|
RateLimitSecs int `json:"rate_limit_secs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadQueueInfo represents the complete download queue state
|
|
||||||
type DownloadQueueInfo struct {
|
type DownloadQueueInfo struct {
|
||||||
IsDownloading bool `json:"is_downloading"`
|
IsDownloading bool `json:"is_downloading"`
|
||||||
Queue []DownloadItem `json:"queue"`
|
Queue []DownloadItem `json:"queue"`
|
||||||
CurrentSpeed float64 `json:"current_speed"` // MB/s
|
CurrentSpeed float64 `json:"current_speed"`
|
||||||
TotalDownloaded float64 `json:"total_downloaded"` // MB this session
|
TotalDownloaded float64 `json:"total_downloaded"`
|
||||||
SessionStartTime int64 `json:"session_start_time"` // Unix timestamp
|
SessionStartTime int64 `json:"session_start_time"`
|
||||||
QueuedCount int `json:"queued_count"`
|
QueuedCount int `json:"queued_count"`
|
||||||
CompletedCount int `json:"completed_count"`
|
CompletedCount int `json:"completed_count"`
|
||||||
FailedCount int `json:"failed_count"`
|
FailedCount int `json:"failed_count"`
|
||||||
SkippedCount int `json:"skipped_count"`
|
SkippedCount int `json:"skipped_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDownloadProgress returns current download progress
|
|
||||||
func GetDownloadProgress() ProgressInfo {
|
func GetDownloadProgress() ProgressInfo {
|
||||||
downloadingLock.RLock()
|
downloadingLock.RLock()
|
||||||
downloading := isDownloading
|
downloading := isDownloading
|
||||||
@@ -89,41 +87,70 @@ func GetDownloadProgress() ProgressInfo {
|
|||||||
speed := currentSpeed
|
speed := currentSpeed
|
||||||
speedLock.RUnlock()
|
speedLock.RUnlock()
|
||||||
|
|
||||||
|
rateLimitLock.RLock()
|
||||||
|
untilMs := rateLimitUntilMs
|
||||||
|
rateLimitLock.RUnlock()
|
||||||
|
|
||||||
|
rateLimited := false
|
||||||
|
rateLimitSecs := 0
|
||||||
|
if untilMs > 0 {
|
||||||
|
remainingMs := untilMs - getCurrentTimeMillis()
|
||||||
|
if remainingMs > 0 {
|
||||||
|
rateLimited = true
|
||||||
|
rateLimitSecs = int((remainingMs + 999) / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ProgressInfo{
|
return ProgressInfo{
|
||||||
IsDownloading: downloading,
|
IsDownloading: downloading,
|
||||||
MBDownloaded: progress,
|
MBDownloaded: progress,
|
||||||
SpeedMBps: speed,
|
SpeedMBps: speed,
|
||||||
|
RateLimited: rateLimited,
|
||||||
|
RateLimitSecs: rateLimitSecs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDownloadSpeed updates the current download speed
|
func SetRateLimitCooldown(seconds float64) {
|
||||||
|
rateLimitLock.Lock()
|
||||||
|
if seconds <= 0 {
|
||||||
|
rateLimitUntilMs = 0
|
||||||
|
} else {
|
||||||
|
rateLimitUntilMs = getCurrentTimeMillis() + int64(seconds*1000)
|
||||||
|
}
|
||||||
|
rateLimitLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearRateLimitCooldown() {
|
||||||
|
rateLimitLock.Lock()
|
||||||
|
rateLimitUntilMs = 0
|
||||||
|
rateLimitLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func SetDownloadSpeed(mbps float64) {
|
func SetDownloadSpeed(mbps float64) {
|
||||||
speedLock.Lock()
|
speedLock.Lock()
|
||||||
currentSpeed = mbps
|
currentSpeed = mbps
|
||||||
speedLock.Unlock()
|
speedLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDownloadProgress updates the current download progress
|
|
||||||
func SetDownloadProgress(mbDownloaded float64) {
|
func SetDownloadProgress(mbDownloaded float64) {
|
||||||
currentProgressLock.Lock()
|
currentProgressLock.Lock()
|
||||||
currentProgress = mbDownloaded
|
currentProgress = mbDownloaded
|
||||||
currentProgressLock.Unlock()
|
currentProgressLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDownloading sets the downloading state
|
|
||||||
func SetDownloading(downloading bool) {
|
func SetDownloading(downloading bool) {
|
||||||
downloadingLock.Lock()
|
downloadingLock.Lock()
|
||||||
isDownloading = downloading
|
isDownloading = downloading
|
||||||
downloadingLock.Unlock()
|
downloadingLock.Unlock()
|
||||||
|
|
||||||
if !downloading {
|
if !downloading {
|
||||||
// Reset progress when download completes
|
|
||||||
SetDownloadProgress(0)
|
SetDownloadProgress(0)
|
||||||
SetDownloadSpeed(0)
|
SetDownloadSpeed(0)
|
||||||
|
ClearRateLimitCooldown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProgressWriter wraps an io.Writer and reports download progress
|
|
||||||
type ProgressWriter struct {
|
type ProgressWriter struct {
|
||||||
writer io.Writer
|
writer io.Writer
|
||||||
total int64
|
total int64
|
||||||
@@ -131,7 +158,7 @@ type ProgressWriter struct {
|
|||||||
startTime int64
|
startTime int64
|
||||||
lastTime int64
|
lastTime int64
|
||||||
lastBytes int64
|
lastBytes int64
|
||||||
itemID string // Track which download item this belongs to
|
itemID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProgressWriter(writer io.Writer) *ProgressWriter {
|
func NewProgressWriter(writer io.Writer) *ProgressWriter {
|
||||||
@@ -147,7 +174,6 @@ func NewProgressWriter(writer io.Writer) *ProgressWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProgressWriterWithID creates a progress writer with an item ID for queue tracking
|
|
||||||
func NewProgressWriterWithID(writer io.Writer, itemID string) *ProgressWriter {
|
func NewProgressWriterWithID(writer io.Writer, itemID string) *ProgressWriter {
|
||||||
pw := NewProgressWriter(writer)
|
pw := NewProgressWriter(writer)
|
||||||
pw.itemID = itemID
|
pw.itemID = itemID
|
||||||
@@ -159,16 +185,18 @@ func getCurrentTimeMillis() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pw *ProgressWriter) Write(p []byte) (int, error) {
|
func (pw *ProgressWriter) Write(p []byte) (int, error) {
|
||||||
|
if err := CheckDownloadCancelled(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
n, err := pw.writer.Write(p)
|
n, err := pw.writer.Write(p)
|
||||||
pw.total += int64(n)
|
pw.total += int64(n)
|
||||||
|
|
||||||
// Report progress every 256KB for smoother updates
|
|
||||||
if pw.total-pw.lastPrinted >= 256*1024 {
|
if pw.total-pw.lastPrinted >= 256*1024 {
|
||||||
mbDownloaded := float64(pw.total) / (1024 * 1024)
|
mbDownloaded := float64(pw.total) / (1024 * 1024)
|
||||||
|
|
||||||
// Calculate speed (MB/s)
|
|
||||||
now := getCurrentTimeMillis()
|
now := getCurrentTimeMillis()
|
||||||
timeDiff := float64(now-pw.lastTime) / 1000.0 // seconds
|
timeDiff := float64(now-pw.lastTime) / 1000.0
|
||||||
bytesDiff := float64(pw.total - pw.lastBytes)
|
bytesDiff := float64(pw.total - pw.lastBytes)
|
||||||
|
|
||||||
var speedMBps float64
|
var speedMBps float64
|
||||||
@@ -180,10 +208,8 @@ func (pw *ProgressWriter) Write(p []byte) (int, error) {
|
|||||||
fmt.Printf("\rDownloaded: %.2f MB", mbDownloaded)
|
fmt.Printf("\rDownloaded: %.2f MB", mbDownloaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update global progress
|
|
||||||
SetDownloadProgress(mbDownloaded)
|
SetDownloadProgress(mbDownloaded)
|
||||||
|
|
||||||
// Update individual item progress if we have an item ID
|
|
||||||
if pw.itemID != "" {
|
if pw.itemID != "" {
|
||||||
UpdateItemProgress(pw.itemID, mbDownloaded, speedMBps)
|
UpdateItemProgress(pw.itemID, mbDownloaded, speedMBps)
|
||||||
}
|
}
|
||||||
@@ -200,10 +226,7 @@ func (pw *ProgressWriter) GetTotal() int64 {
|
|||||||
return pw.total
|
return pw.total
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue management functions
|
func AddToQueue(id, trackName, artistName, albumName, spotifyID string) {
|
||||||
|
|
||||||
// AddToQueue adds a new item to the download queue
|
|
||||||
func AddToQueue(id, trackName, artistName, albumName, isrc string) {
|
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
defer downloadQueueLock.Unlock()
|
defer downloadQueueLock.Unlock()
|
||||||
|
|
||||||
@@ -212,7 +235,7 @@ func AddToQueue(id, trackName, artistName, albumName, isrc string) {
|
|||||||
TrackName: trackName,
|
TrackName: trackName,
|
||||||
ArtistName: artistName,
|
ArtistName: artistName,
|
||||||
AlbumName: albumName,
|
AlbumName: albumName,
|
||||||
ISRC: isrc,
|
SpotifyID: spotifyID,
|
||||||
Status: StatusQueued,
|
Status: StatusQueued,
|
||||||
Progress: 0,
|
Progress: 0,
|
||||||
TotalSize: 0,
|
TotalSize: 0,
|
||||||
@@ -223,7 +246,6 @@ func AddToQueue(id, trackName, artistName, albumName, isrc string) {
|
|||||||
|
|
||||||
downloadQueue = append(downloadQueue, item)
|
downloadQueue = append(downloadQueue, item)
|
||||||
|
|
||||||
// Initialize session start time if this is the first item
|
|
||||||
sessionStartLock.Lock()
|
sessionStartLock.Lock()
|
||||||
if sessionStartTime == 0 {
|
if sessionStartTime == 0 {
|
||||||
sessionStartTime = time.Now().Unix()
|
sessionStartTime = time.Now().Unix()
|
||||||
@@ -231,7 +253,6 @@ func AddToQueue(id, trackName, artistName, albumName, isrc string) {
|
|||||||
sessionStartLock.Unlock()
|
sessionStartLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartDownloadItem marks an item as currently downloading
|
|
||||||
func StartDownloadItem(id string) {
|
func StartDownloadItem(id string) {
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
defer downloadQueueLock.Unlock()
|
defer downloadQueueLock.Unlock()
|
||||||
@@ -250,7 +271,6 @@ func StartDownloadItem(id string) {
|
|||||||
currentItemLock.Unlock()
|
currentItemLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateItemProgress updates the progress of the current download item
|
|
||||||
func UpdateItemProgress(id string, progress, speed float64) {
|
func UpdateItemProgress(id string, progress, speed float64) {
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
defer downloadQueueLock.Unlock()
|
defer downloadQueueLock.Unlock()
|
||||||
@@ -264,14 +284,12 @@ func UpdateItemProgress(id string, progress, speed float64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentItemID returns the ID of the currently downloading item
|
|
||||||
func GetCurrentItemID() string {
|
func GetCurrentItemID() string {
|
||||||
currentItemLock.RLock()
|
currentItemLock.RLock()
|
||||||
defer currentItemLock.RUnlock()
|
defer currentItemLock.RUnlock()
|
||||||
return currentItemID
|
return currentItemID
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompleteDownloadItem marks an item as completed
|
|
||||||
func CompleteDownloadItem(id, filePath string, finalSize float64) {
|
func CompleteDownloadItem(id, filePath string, finalSize float64) {
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
defer downloadQueueLock.Unlock()
|
defer downloadQueueLock.Unlock()
|
||||||
@@ -284,7 +302,6 @@ func CompleteDownloadItem(id, filePath string, finalSize float64) {
|
|||||||
downloadQueue[i].Progress = finalSize
|
downloadQueue[i].Progress = finalSize
|
||||||
downloadQueue[i].TotalSize = finalSize
|
downloadQueue[i].TotalSize = finalSize
|
||||||
|
|
||||||
// Add to total downloaded
|
|
||||||
totalDownloadedLock.Lock()
|
totalDownloadedLock.Lock()
|
||||||
totalDownloaded += finalSize
|
totalDownloaded += finalSize
|
||||||
totalDownloadedLock.Unlock()
|
totalDownloadedLock.Unlock()
|
||||||
@@ -293,7 +310,6 @@ func CompleteDownloadItem(id, filePath string, finalSize float64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FailDownloadItem marks an item as failed
|
|
||||||
func FailDownloadItem(id, errorMsg string) {
|
func FailDownloadItem(id, errorMsg string) {
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
defer downloadQueueLock.Unlock()
|
defer downloadQueueLock.Unlock()
|
||||||
@@ -308,7 +324,6 @@ func FailDownloadItem(id, errorMsg string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SkipDownloadItem marks an item as skipped (already exists)
|
|
||||||
func SkipDownloadItem(id, filePath string) {
|
func SkipDownloadItem(id, filePath string) {
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
defer downloadQueueLock.Unlock()
|
defer downloadQueueLock.Unlock()
|
||||||
@@ -323,9 +338,8 @@ func SkipDownloadItem(id, filePath string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDownloadQueue returns the complete download queue state
|
|
||||||
func GetDownloadQueue() DownloadQueueInfo {
|
func GetDownloadQueue() DownloadQueueInfo {
|
||||||
// Auto-reset session if all downloads are complete
|
|
||||||
ResetSessionIfComplete()
|
ResetSessionIfComplete()
|
||||||
|
|
||||||
downloadQueueLock.RLock()
|
downloadQueueLock.RLock()
|
||||||
@@ -347,7 +361,6 @@ func GetDownloadQueue() DownloadQueueInfo {
|
|||||||
sessionStart := sessionStartTime
|
sessionStart := sessionStartTime
|
||||||
sessionStartLock.RUnlock()
|
sessionStartLock.RUnlock()
|
||||||
|
|
||||||
// Count statuses
|
|
||||||
var queued, completed, failed, skipped int
|
var queued, completed, failed, skipped int
|
||||||
for _, item := range downloadQueue {
|
for _, item := range downloadQueue {
|
||||||
switch item.Status {
|
switch item.Status {
|
||||||
@@ -362,7 +375,6 @@ func GetDownloadQueue() DownloadQueueInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a copy of the queue
|
|
||||||
queueCopy := make([]DownloadItem, len(downloadQueue))
|
queueCopy := make([]DownloadItem, len(downloadQueue))
|
||||||
copy(queueCopy, downloadQueue)
|
copy(queueCopy, downloadQueue)
|
||||||
|
|
||||||
@@ -379,12 +391,10 @@ func GetDownloadQueue() DownloadQueueInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearDownloadQueue clears all completed, failed, and skipped items from the queue
|
|
||||||
func ClearDownloadQueue() {
|
func ClearDownloadQueue() {
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
defer downloadQueueLock.Unlock()
|
defer downloadQueueLock.Unlock()
|
||||||
|
|
||||||
// Keep only queued and downloading items
|
|
||||||
newQueue := make([]DownloadItem, 0)
|
newQueue := make([]DownloadItem, 0)
|
||||||
for _, item := range downloadQueue {
|
for _, item := range downloadQueue {
|
||||||
if item.Status == StatusQueued || item.Status == StatusDownloading {
|
if item.Status == StatusQueued || item.Status == StatusDownloading {
|
||||||
@@ -394,7 +404,6 @@ func ClearDownloadQueue() {
|
|||||||
downloadQueue = newQueue
|
downloadQueue = newQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearAllDownloads clears the entire queue and resets session stats
|
|
||||||
func ClearAllDownloads() {
|
func ClearAllDownloads() {
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
downloadQueue = []DownloadItem{}
|
downloadQueue = []DownloadItem{}
|
||||||
@@ -412,13 +421,10 @@ func ClearAllDownloads() {
|
|||||||
currentItemID = ""
|
currentItemID = ""
|
||||||
currentItemLock.Unlock()
|
currentItemLock.Unlock()
|
||||||
|
|
||||||
// Reset current progress and speed
|
|
||||||
SetDownloadProgress(0)
|
SetDownloadProgress(0)
|
||||||
SetDownloadSpeed(0)
|
SetDownloadSpeed(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelAllQueuedItems marks all queued items as skipped (cancelled)
|
|
||||||
// This is called when user stops a download or when batch download completes
|
|
||||||
func CancelAllQueuedItems() {
|
func CancelAllQueuedItems() {
|
||||||
downloadQueueLock.Lock()
|
downloadQueueLock.Lock()
|
||||||
defer downloadQueueLock.Unlock()
|
defer downloadQueueLock.Unlock()
|
||||||
@@ -432,8 +438,25 @@ func CancelAllQueuedItems() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetSessionIfComplete resets session stats if no active or queued downloads
|
func CancelQueuedAndDownloadingItems() {
|
||||||
// Note: Does NOT clear the queue - items remain visible for history
|
downloadQueueLock.Lock()
|
||||||
|
for i := range downloadQueue {
|
||||||
|
if downloadQueue[i].Status == StatusQueued || downloadQueue[i].Status == StatusDownloading {
|
||||||
|
downloadQueue[i].Status = StatusSkipped
|
||||||
|
downloadQueue[i].EndTime = time.Now().Unix()
|
||||||
|
downloadQueue[i].ErrorMessage = "Cancelled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadQueueLock.Unlock()
|
||||||
|
|
||||||
|
currentItemLock.Lock()
|
||||||
|
currentItemID = ""
|
||||||
|
currentItemLock.Unlock()
|
||||||
|
|
||||||
|
SetDownloadProgress(0)
|
||||||
|
SetDownloadSpeed(0)
|
||||||
|
}
|
||||||
|
|
||||||
func ResetSessionIfComplete() {
|
func ResetSessionIfComplete() {
|
||||||
downloadQueueLock.RLock()
|
downloadQueueLock.RLock()
|
||||||
hasActiveOrQueued := false
|
hasActiveOrQueued := false
|
||||||
@@ -445,8 +468,6 @@ func ResetSessionIfComplete() {
|
|||||||
}
|
}
|
||||||
downloadQueueLock.RUnlock()
|
downloadQueueLock.RUnlock()
|
||||||
|
|
||||||
// If no active or queued items, reset session stats
|
|
||||||
// But keep the queue items for history visibility
|
|
||||||
if !hasActiveOrQueued {
|
if !hasActiveOrQueued {
|
||||||
sessionStartLock.Lock()
|
sessionStartLock.Lock()
|
||||||
sessionStartTime = 0
|
sessionStartTime = 0
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const amazonMusicAPIBaseURL = "https://amazon.spotbye.qzz.io"
|
||||||
|
|
||||||
|
const (
|
||||||
|
qobuzWJHEBaseURL = "https://music.wjhe.top"
|
||||||
|
qobuzWJHESearchAPIURL = qobuzWJHEBaseURL + "/api/music/qobuz/search"
|
||||||
|
qobuzWJHEStreamAPIURL = qobuzWJHEBaseURL + "/api/music/qobuz/url"
|
||||||
|
qobuzMusicDLDownloadAPIURL = "https://www.musicdl.me/api/qobuz/download"
|
||||||
|
qobuzGDStudioAPIURLXYZ = "https://music.gdstudio.xyz/api.php"
|
||||||
|
qobuzGDStudioAPIURLORG = "https://music.gdstudio.org/api.php"
|
||||||
|
qobuzGDStudioVersion = "2026.5.10"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultQobuzDownloadProviderURLs = []string{
|
||||||
|
qobuzWJHEStreamAPIURL,
|
||||||
|
qobuzGDStudioAPIURLXYZ,
|
||||||
|
qobuzGDStudioAPIURLORG,
|
||||||
|
qobuzMusicDLDownloadAPIURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzDownloadProviderURLs() []string {
|
||||||
|
return append([]string(nil), defaultQobuzDownloadProviderURLs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzWJHESearchAPIURL() string {
|
||||||
|
return qobuzWJHESearchAPIURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzWJHEStreamAPIURL() string {
|
||||||
|
return qobuzWJHEStreamAPIURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzMusicDLDownloadAPIURL() string {
|
||||||
|
return qobuzMusicDLDownloadAPIURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzGDStudioAPIURLs() []string {
|
||||||
|
return []string{qobuzGDStudioAPIURLXYZ, qobuzGDStudioAPIURLORG}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzGDStudioPrimaryAPIURL() string {
|
||||||
|
return qobuzGDStudioAPIURLXYZ
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzGDStudioFallbackAPIURL() string {
|
||||||
|
return qobuzGDStudioAPIURLORG
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzGDStudioSignatureHost(apiURL string) string {
|
||||||
|
parsed, err := url.Parse(strings.TrimSpace(apiURL))
|
||||||
|
if err != nil || strings.TrimSpace(parsed.Host) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(parsed.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQobuzGDStudioVersion() string {
|
||||||
|
return qobuzGDStudioVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsQobuzWJHEProviderURL(raw string) bool {
|
||||||
|
candidate := strings.TrimSpace(raw)
|
||||||
|
return candidate == qobuzWJHEStreamAPIURL || strings.HasPrefix(candidate, qobuzWJHEStreamAPIURL+"?")
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsQobuzMusicDLProviderURL(raw string) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(raw), qobuzMusicDLDownloadAPIURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsQobuzGDStudioProviderURL(raw string) bool {
|
||||||
|
candidate := strings.TrimSpace(raw)
|
||||||
|
for _, apiURL := range GetQobuzGDStudioAPIURLs() {
|
||||||
|
if candidate == apiURL || strings.HasPrefix(candidate, apiURL+"?") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAmazonMusicAPIBaseURL() string {
|
||||||
|
return amazonMusicAPIBaseURL
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
providerPriorityDBFile = "provider_priority.db"
|
||||||
|
providerPriorityBucket = "ProviderPriority"
|
||||||
|
)
|
||||||
|
|
||||||
|
type providerPriorityEntry struct {
|
||||||
|
Service string `json:"service"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
LastOutcome string `json:"last_outcome"`
|
||||||
|
LastAttempt int64 `json:"last_attempt"`
|
||||||
|
LastSuccess int64 `json:"last_success"`
|
||||||
|
LastFailure int64 `json:"last_failure"`
|
||||||
|
SuccessCount int64 `json:"success_count"`
|
||||||
|
FailureCount int64 `json:"failure_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
providerPriorityDB *bolt.DB
|
||||||
|
providerPriorityDBMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitProviderPriorityDB() error {
|
||||||
|
providerPriorityDBMu.Lock()
|
||||||
|
defer providerPriorityDBMu.Unlock()
|
||||||
|
|
||||||
|
if providerPriorityDB != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appDir, err := EnsureAppDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := filepath.Join(appDir, providerPriorityDBFile)
|
||||||
|
db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
_, err := tx.CreateBucketIfNotExists([]byte(providerPriorityBucket))
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
providerPriorityDB = db
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseProviderPriorityDB() {
|
||||||
|
providerPriorityDBMu.Lock()
|
||||||
|
defer providerPriorityDBMu.Unlock()
|
||||||
|
|
||||||
|
if providerPriorityDB != nil {
|
||||||
|
_ = providerPriorityDB.Close()
|
||||||
|
providerPriorityDB = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prioritizeProviders(service string, providers []string) []string {
|
||||||
|
ordered := append([]string(nil), providers...)
|
||||||
|
if len(ordered) < 2 {
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := InitProviderPriorityDB(); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to init provider priority DB: %v\n", err)
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceKey := strings.TrimSpace(strings.ToLower(service))
|
||||||
|
entries := make(map[string]providerPriorityEntry, len(ordered))
|
||||||
|
|
||||||
|
if err := providerPriorityDB.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(providerPriorityBucket))
|
||||||
|
if bucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, provider := range ordered {
|
||||||
|
if raw := bucket.Get([]byte(providerPriorityKey(serviceKey, provider))); len(raw) > 0 {
|
||||||
|
var entry providerPriorityEntry
|
||||||
|
if err := json.Unmarshal(raw, &entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entries[provider] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to read provider priority DB: %v\n", err)
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
originalIndex := make(map[string]int, len(ordered))
|
||||||
|
for idx, provider := range ordered {
|
||||||
|
originalIndex[provider] = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(ordered, func(i, j int) bool {
|
||||||
|
left := entries[ordered[i]]
|
||||||
|
right := entries[ordered[j]]
|
||||||
|
|
||||||
|
leftRank := providerOutcomeRank(left.LastOutcome)
|
||||||
|
rightRank := providerOutcomeRank(right.LastOutcome)
|
||||||
|
if leftRank != rightRank {
|
||||||
|
return leftRank > rightRank
|
||||||
|
}
|
||||||
|
|
||||||
|
if left.LastSuccess != right.LastSuccess {
|
||||||
|
return left.LastSuccess > right.LastSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
if left.LastAttempt != right.LastAttempt {
|
||||||
|
return left.LastAttempt > right.LastAttempt
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalIndex[ordered[i]] < originalIndex[ordered[j]]
|
||||||
|
})
|
||||||
|
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordProviderSuccess(service string, provider string) {
|
||||||
|
recordProviderOutcome(service, provider, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordProviderFailure(service string, provider string) {
|
||||||
|
recordProviderOutcome(service, provider, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordProviderOutcome(service string, provider string, success bool) {
|
||||||
|
if strings.TrimSpace(service) == "" || strings.TrimSpace(provider) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := InitProviderPriorityDB(); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to init provider priority DB: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceKey := strings.TrimSpace(strings.ToLower(service))
|
||||||
|
providerKey := providerPriorityKey(serviceKey, provider)
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
if err := providerPriorityDB.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := tx.CreateBucketIfNotExists([]byte(providerPriorityBucket))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := providerPriorityEntry{
|
||||||
|
Service: serviceKey,
|
||||||
|
Provider: provider,
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw := bucket.Get([]byte(providerKey)); len(raw) > 0 {
|
||||||
|
if err := json.Unmarshal(raw, &entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.LastAttempt = now
|
||||||
|
if success {
|
||||||
|
entry.LastOutcome = "success"
|
||||||
|
entry.LastSuccess = now
|
||||||
|
entry.SuccessCount++
|
||||||
|
} else {
|
||||||
|
entry.LastOutcome = "failure"
|
||||||
|
entry.LastFailure = now
|
||||||
|
entry.FailureCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Put([]byte(providerKey), payload)
|
||||||
|
}); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to update provider priority DB: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func providerOutcomeRank(outcome string) int {
|
||||||
|
switch strings.TrimSpace(strings.ToLower(outcome)) {
|
||||||
|
case "success":
|
||||||
|
return 2
|
||||||
|
case "":
|
||||||
|
return 1
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func providerPriorityKey(service string, provider string) string {
|
||||||
|
return strings.TrimSpace(strings.ToLower(service)) + "|" + strings.TrimSpace(provider)
|
||||||
|
}
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
qobuzAPIBaseURL = "https://www.qobuz.com/api.json/0.2"
|
||||||
|
qobuzDefaultAPIAppID = "712109809"
|
||||||
|
qobuzDefaultAPIAppSecret = "589be88e4538daea11f509d29e4a23b1"
|
||||||
|
qobuzDefaultUA = DefaultDownloaderUserAgent
|
||||||
|
qobuzCredentialsCacheFile = "qobuz-api-credentials.json"
|
||||||
|
qobuzCredentialsCacheTTL = 24 * time.Hour
|
||||||
|
qobuzCredentialsProbeTrackISRC = "USUM71703861"
|
||||||
|
qobuzOpenTrackProbeURL = "https://open.qobuz.com/track/1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
qobuzCredentialsMu sync.Mutex
|
||||||
|
qobuzCachedCredentials *qobuzAPICredentials
|
||||||
|
qobuzOpenBundleScriptPattern = regexp.MustCompile(`<script[^>]+src="([^"]+/js/main\.js|/resources/[^"]+/js/main\.js)"`)
|
||||||
|
qobuzOpenAPIConfigPattern = regexp.MustCompile(`app_id:"(?P<app_id>\d{9})",app_secret:"(?P<app_secret>[a-f0-9]{32})"`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type qobuzAPICredentials struct {
|
||||||
|
AppID string `json:"app_id"`
|
||||||
|
AppSecret string `json:"app_secret"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
FetchedAtUnix int64 `json:"fetched_at_unix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type qobuzCredentialProbeResponse struct {
|
||||||
|
Tracks struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
} `json:"tracks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultQobuzAPICredentials() *qobuzAPICredentials {
|
||||||
|
return &qobuzAPICredentials{
|
||||||
|
AppID: qobuzDefaultAPIAppID,
|
||||||
|
AppSecret: qobuzDefaultAPIAppSecret,
|
||||||
|
Source: "embedded-default",
|
||||||
|
FetchedAtUnix: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzCredentialsCachePath() (string, error) {
|
||||||
|
appDir, err := GetFFmpegDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(appDir, qobuzCredentialsCacheFile), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadQobuzCachedCredentials() (*qobuzAPICredentials, error) {
|
||||||
|
cachePath, err := qobuzCredentialsCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := os.ReadFile(cachePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read qobuz credentials cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var creds qobuzAPICredentials
|
||||||
|
if err := json.Unmarshal(body, &creds); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse qobuz credentials cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(creds.AppID) == "" || strings.TrimSpace(creds.AppSecret) == "" {
|
||||||
|
return nil, fmt.Errorf("qobuz credentials cache is incomplete")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &creds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveQobuzCachedCredentials(creds *qobuzAPICredentials) error {
|
||||||
|
if creds == nil {
|
||||||
|
return fmt.Errorf("qobuz credentials are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cachePath, err := qobuzCredentialsCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(cachePath), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create qobuz credentials cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.MarshalIndent(creds, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(cachePath, body, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write qobuz credentials cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzCredentialsCacheIsFresh(creds *qobuzAPICredentials) bool {
|
||||||
|
if creds == nil || creds.FetchedAtUnix == 0 || strings.TrimSpace(creds.AppID) == "" || strings.TrimSpace(creds.AppSecret) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return time.Since(time.Unix(creds.FetchedAtUnix, 0)) < qobuzCredentialsCacheTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrapeQobuzOpenCredentials(client *http.Client) (*qobuzAPICredentials, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, qobuzOpenTrackProbeURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", qobuzDefaultUA)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch open.qobuz.com shell: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
preview, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||||
|
return nil, fmt.Errorf("open.qobuz.com returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(preview)))
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read open.qobuz.com shell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptMatch := qobuzOpenBundleScriptPattern.FindStringSubmatch(string(htmlBody))
|
||||||
|
if len(scriptMatch) < 2 {
|
||||||
|
return nil, fmt.Errorf("qobuz open bundle URL not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleURL := strings.TrimSpace(scriptMatch[1])
|
||||||
|
if strings.HasPrefix(bundleURL, "/") {
|
||||||
|
bundleURL = "https://open.qobuz.com" + bundleURL
|
||||||
|
}
|
||||||
|
if bundleURL == "" {
|
||||||
|
return nil, fmt.Errorf("qobuz open bundle URL is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleReq, err := http.NewRequest(http.MethodGet, bundleURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bundleReq.Header.Set("User-Agent", qobuzDefaultUA)
|
||||||
|
|
||||||
|
bundleResp, err := client.Do(bundleReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch qobuz open bundle: %w", err)
|
||||||
|
}
|
||||||
|
defer bundleResp.Body.Close()
|
||||||
|
|
||||||
|
if bundleResp.StatusCode != http.StatusOK {
|
||||||
|
preview, _ := io.ReadAll(io.LimitReader(bundleResp.Body, 512))
|
||||||
|
return nil, fmt.Errorf("qobuz open bundle returned status %d: %s", bundleResp.StatusCode, strings.TrimSpace(string(preview)))
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleBody, err := io.ReadAll(bundleResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read qobuz open bundle: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configMatch := qobuzOpenAPIConfigPattern.FindStringSubmatch(string(bundleBody))
|
||||||
|
if len(configMatch) < 3 {
|
||||||
|
return nil, fmt.Errorf("qobuz api app_id/app_secret pair not found in open bundle")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &qobuzAPICredentials{
|
||||||
|
AppID: strings.TrimSpace(configMatch[1]),
|
||||||
|
AppSecret: strings.TrimSpace(configMatch[2]),
|
||||||
|
Source: bundleURL,
|
||||||
|
FetchedAtUnix: time.Now().Unix(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzNormalizedPath(path string) string {
|
||||||
|
return strings.Trim(strings.TrimSpace(path), "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzSignaturePayload(path string, params url.Values, timestamp string, secret string) string {
|
||||||
|
normalizedPath := strings.ReplaceAll(qobuzNormalizedPath(path), "/", "")
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for key := range params {
|
||||||
|
switch key {
|
||||||
|
case "app_id", "request_ts", "request_sig":
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(normalizedPath)
|
||||||
|
for _, key := range keys {
|
||||||
|
values := params[key]
|
||||||
|
if len(values) == 0 {
|
||||||
|
builder.WriteString(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
builder.WriteString(key)
|
||||||
|
builder.WriteString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.WriteString(timestamp)
|
||||||
|
builder.WriteString(secret)
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzRequestSignature(path string, params url.Values, timestamp string, secret string) string {
|
||||||
|
sum := md5.Sum([]byte(qobuzSignaturePayload(path, params, timestamp, secret)))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func newQobuzSignedRequestWithCredentials(method string, path string, params url.Values, creds *qobuzAPICredentials) (*http.Request, error) {
|
||||||
|
normalizedPath := qobuzNormalizedPath(path)
|
||||||
|
if normalizedPath == "" {
|
||||||
|
return nil, fmt.Errorf("qobuz request path is empty")
|
||||||
|
}
|
||||||
|
if creds == nil || strings.TrimSpace(creds.AppID) == "" || strings.TrimSpace(creds.AppSecret) == "" {
|
||||||
|
return nil, fmt.Errorf("qobuz credentials are incomplete")
|
||||||
|
}
|
||||||
|
|
||||||
|
clonedParams := url.Values{}
|
||||||
|
for key, values := range params {
|
||||||
|
for _, value := range values {
|
||||||
|
clonedParams.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
||||||
|
clonedParams.Set("app_id", creds.AppID)
|
||||||
|
clonedParams.Set("request_ts", timestamp)
|
||||||
|
clonedParams.Set("request_sig", qobuzRequestSignature(normalizedPath, params, timestamp, creds.AppSecret))
|
||||||
|
|
||||||
|
reqURL := fmt.Sprintf("%s/%s?%s", qobuzAPIBaseURL, normalizedPath, clonedParams.Encode())
|
||||||
|
req, err := http.NewRequest(method, reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", qobuzDefaultUA)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("X-App-Id", creds.AppID)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzCredentialsSupportSignedMetadata(client *http.Client, creds *qobuzAPICredentials) bool {
|
||||||
|
if creds == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := newQobuzSignedRequestWithCredentials(http.MethodGet, "track/search", url.Values{
|
||||||
|
"query": {qobuzCredentialsProbeTrackISRC},
|
||||||
|
"limit": {"1"},
|
||||||
|
}, creds)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload qobuzCredentialProbeResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload.Tracks.Total > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getQobuzAPICredentials(forceRefresh bool) (*qobuzAPICredentials, error) {
|
||||||
|
qobuzCredentialsMu.Lock()
|
||||||
|
defer qobuzCredentialsMu.Unlock()
|
||||||
|
|
||||||
|
if !forceRefresh && qobuzCredentialsCacheIsFresh(qobuzCachedCredentials) {
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedFromDisk, diskErr := loadQobuzCachedCredentials()
|
||||||
|
if diskErr != nil {
|
||||||
|
fmt.Printf("Warning: failed to read Qobuz credentials cache: %v\n", diskErr)
|
||||||
|
}
|
||||||
|
if !forceRefresh && qobuzCredentialsCacheIsFresh(cachedFromDisk) {
|
||||||
|
qobuzCachedCredentials = cachedFromDisk
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
scrapedCreds, scrapeErr := scrapeQobuzOpenCredentials(client)
|
||||||
|
if scrapeErr == nil {
|
||||||
|
if qobuzCredentialsSupportSignedMetadata(client, scrapedCreds) {
|
||||||
|
qobuzCachedCredentials = scrapedCreds
|
||||||
|
if err := saveQobuzCachedCredentials(scrapedCreds); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to write Qobuz credentials cache: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Loaded fresh Qobuz credentials from %s (app_id=%s)\n", scrapedCreds.Source, scrapedCreds.AppID)
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
scrapeErr = fmt.Errorf("scraped qobuz credentials did not pass validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cachedFromDisk != nil {
|
||||||
|
qobuzCachedCredentials = cachedFromDisk
|
||||||
|
fmt.Printf("Warning: failed to refresh Qobuz credentials, using cached credentials: %v\n", scrapeErr)
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if qobuzCachedCredentials != nil {
|
||||||
|
fmt.Printf("Warning: failed to refresh Qobuz credentials, using in-memory credentials: %v\n", scrapeErr)
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fallback := defaultQobuzAPICredentials()
|
||||||
|
qobuzCachedCredentials = fallback
|
||||||
|
if scrapeErr != nil {
|
||||||
|
fmt.Printf("Warning: failed to refresh Qobuz credentials, using embedded fallback: %v\n", scrapeErr)
|
||||||
|
}
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzShouldRefreshCredentials(statusCode int) bool {
|
||||||
|
return statusCode == http.StatusBadRequest || statusCode == http.StatusUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
func newQobuzSignedRequest(method string, path string, params url.Values) (*http.Request, error) {
|
||||||
|
creds, err := getQobuzAPICredentials(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newQobuzSignedRequestWithCredentials(method, path, params, creds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doQobuzSignedRequest(method string, path string, params url.Values, client *http.Client) (*http.Response, error) {
|
||||||
|
if client == nil {
|
||||||
|
client = &http.Client{Timeout: 20 * time.Second}
|
||||||
|
}
|
||||||
|
|
||||||
|
call := func(forceRefresh bool) (*http.Response, error) {
|
||||||
|
creds, err := getQobuzAPICredentials(forceRefresh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := newQobuzSignedRequestWithCredentials(method, path, params, creds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := call(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if qobuzShouldRefreshCredentials(resp.StatusCode) {
|
||||||
|
resp.Body.Close()
|
||||||
|
return call(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doQobuzSignedJSONRequest(path string, params url.Values, target interface{}) error {
|
||||||
|
resp, err := doQobuzSignedRequest(http.MethodGet, path, params, &http.Client{Timeout: 20 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
||||||
|
return fmt.Errorf("qobuz request failed: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(snippet)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.NewDecoder(resp.Body).Decode(target)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mapQobuzQualityToCommunity(quality string) string {
|
||||||
|
switch strings.TrimSpace(quality) {
|
||||||
|
case "27", "7":
|
||||||
|
return "24"
|
||||||
|
default:
|
||||||
|
return "16"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QobuzDownloader) getQobuzCommunityDownloadURL(trackID int64, quality string) (string, error) {
|
||||||
|
payload, err := json.Marshal(map[string]string{
|
||||||
|
"id": fmt.Sprintf("%d", trackID),
|
||||||
|
"quality": mapQobuzQualityToCommunity(quality),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := doCommunityRequest(q.client, "Qobuz", func() (*http.Request, error) {
|
||||||
|
req, err := NewRequestWithDefaultHeaders(http.MethodPost, GetQobuzCommunityDownloadURL(), bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if err := setCommunityRequestHeaders(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("qobuz community API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadURL := extractQobuzStreamingURL(body)
|
||||||
|
if downloadURL == "" {
|
||||||
|
return "", fmt.Errorf("no streamable URL in qobuz community response")
|
||||||
|
}
|
||||||
|
return downloadURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QobuzDownloader) getQobuzCustomDownloadURL(trackID int64, quality string) (string, error) {
|
||||||
|
base := strings.TrimRight(strings.TrimSpace(q.customURL), "/")
|
||||||
|
if base == "" {
|
||||||
|
return "", fmt.Errorf("no custom Qobuz instance configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
qualityCode := strings.TrimSpace(quality)
|
||||||
|
switch qualityCode {
|
||||||
|
case "5", "6", "7", "27":
|
||||||
|
default:
|
||||||
|
qualityCode = "27"
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := fmt.Sprintf("%s/api/download-music?track_id=%d&quality=%s", base, trackID, url.QueryEscape(qualityCode))
|
||||||
|
req, err := NewRequestWithDefaultHeaders(http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := q.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("qobuz custom instance returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"data"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode qobuz custom response: %w", err)
|
||||||
|
}
|
||||||
|
if !parsed.Success || strings.TrimSpace(parsed.Data.URL) == "" {
|
||||||
|
if strings.TrimSpace(parsed.Error) != "" {
|
||||||
|
return "", fmt.Errorf("qobuz custom instance error: %s", parsed.Error)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no download URL in qobuz custom response")
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(parsed.Data.URL), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
type qobuzDownloadProvider interface {
|
||||||
|
Name() string
|
||||||
|
Attempts(trackID int64, quality string) []qobuzProviderAttempt
|
||||||
|
}
|
||||||
|
|
||||||
|
type qobuzProviderAttempt struct {
|
||||||
|
Name string
|
||||||
|
ID string
|
||||||
|
Download func() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type QobuzProviderWJHE struct {
|
||||||
|
downloader *QobuzDownloader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p QobuzProviderWJHE) Name() string {
|
||||||
|
return "QobuzProviderWJHE"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p QobuzProviderWJHE) Attempts(trackID int64, quality string) []qobuzProviderAttempt {
|
||||||
|
return []qobuzProviderAttempt{
|
||||||
|
{
|
||||||
|
Name: p.Name(),
|
||||||
|
ID: GetQobuzWJHEStreamAPIURL(),
|
||||||
|
Download: func() (string, error) {
|
||||||
|
return p.downloader.DownloadFromWJHE(trackID, quality)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QobuzProviderMusicDL struct {
|
||||||
|
downloader *QobuzDownloader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p QobuzProviderMusicDL) Name() string {
|
||||||
|
return "QobuzProviderMusicDL"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p QobuzProviderMusicDL) Attempts(trackID int64, quality string) []qobuzProviderAttempt {
|
||||||
|
return []qobuzProviderAttempt{
|
||||||
|
{
|
||||||
|
Name: p.Name(),
|
||||||
|
ID: GetQobuzMusicDLDownloadAPIURL(),
|
||||||
|
Download: func() (string, error) {
|
||||||
|
return p.downloader.DownloadFromMusicDL(trackID, quality)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QobuzProviderGDStudio struct {
|
||||||
|
downloader *QobuzDownloader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p QobuzProviderGDStudio) Name() string {
|
||||||
|
return "QobuzProviderGDStudio"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p QobuzProviderGDStudio) Attempts(trackID int64, quality string) []qobuzProviderAttempt {
|
||||||
|
attempts := make([]qobuzProviderAttempt, 0, len(GetQobuzGDStudioAPIURLs()))
|
||||||
|
for _, apiURL := range GetQobuzGDStudioAPIURLs() {
|
||||||
|
currentAPIURL := apiURL
|
||||||
|
attempts = append(attempts, qobuzProviderAttempt{
|
||||||
|
Name: p.Name(),
|
||||||
|
ID: currentAPIURL,
|
||||||
|
Download: func() (string, error) {
|
||||||
|
return p.downloader.DownloadFromGDStudio(trackID, quality, currentAPIURL)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return attempts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QobuzDownloader) getQobuzDownloadProviders() []qobuzDownloadProvider {
|
||||||
|
return []qobuzDownloadProvider{
|
||||||
|
QobuzProviderWJHE{downloader: q},
|
||||||
|
QobuzProviderGDStudio{downloader: q},
|
||||||
|
QobuzProviderMusicDL{downloader: q},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveQobuzAttemptIDsLast(providerIDs []string, lastIDs ...string) []string {
|
||||||
|
if len(providerIDs) == 0 || len(lastIDs) == 0 {
|
||||||
|
return append([]string(nil), providerIDs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIDSet := make(map[string]struct{}, len(lastIDs))
|
||||||
|
for _, providerID := range lastIDs {
|
||||||
|
lastIDSet[providerID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ordered := make([]string, 0, len(providerIDs))
|
||||||
|
trailing := make([]string, 0, len(providerIDs))
|
||||||
|
for _, providerID := range providerIDs {
|
||||||
|
if _, ok := lastIDSet[providerID]; ok {
|
||||||
|
trailing = append(trailing, providerID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ordered = append(ordered, providerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(ordered, trailing...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const recentFetchesFileName = "recent_fetches.json"
|
||||||
|
|
||||||
|
type RecentFetchItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Artist string `json:"artist"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
IsExplicit bool `json:"is_explicit,omitempty"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
recentFetchesMu sync.Mutex
|
||||||
|
recentFetchesDirResolver = GetFFmpegDir
|
||||||
|
)
|
||||||
|
|
||||||
|
func recentFetchesFilePath() (string, error) {
|
||||||
|
baseDir, err := recentFetchesDirResolver()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(baseDir, 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(baseDir, recentFetchesFileName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadRecentFetches() ([]RecentFetchItem, error) {
|
||||||
|
recentFetchesMu.Lock()
|
||||||
|
defer recentFetchesMu.Unlock()
|
||||||
|
|
||||||
|
filePath, err := recentFetchesFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return []RecentFetchItem{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(string(data)) == "" {
|
||||||
|
return []RecentFetchItem{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []RecentFetchItem
|
||||||
|
if err := json.Unmarshal(data, &items); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if items == nil {
|
||||||
|
return []RecentFetchItem{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveRecentFetches(items []RecentFetchItem) error {
|
||||||
|
recentFetchesMu.Lock()
|
||||||
|
defer recentFetchesMu.Unlock()
|
||||||
|
|
||||||
|
filePath, err := recentFetchesFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if items == nil {
|
||||||
|
items = []RecentFetchItem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(items, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(filePath, data, 0o644)
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FlacInfo struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
SampleRate uint32 `json:"sample_rate"`
|
||||||
|
BitsPerSample uint8 `json:"bits_per_sample"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFlacInfoBatch(paths []string) []FlacInfo {
|
||||||
|
results := make([]FlacInfo, len(paths))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i, path := range paths {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int, p string) {
|
||||||
|
defer wg.Done()
|
||||||
|
info := FlacInfo{Path: p}
|
||||||
|
|
||||||
|
ffprobePath, err := GetFFprobePath()
|
||||||
|
if err != nil {
|
||||||
|
results[idx] = info
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-v", "error",
|
||||||
|
"-select_streams", "a:0",
|
||||||
|
"-show_entries", "stream=sample_rate,bits_per_raw_sample,bits_per_sample",
|
||||||
|
"-of", "default=noprint_wrappers=0",
|
||||||
|
p,
|
||||||
|
}
|
||||||
|
cmd := exec.Command(ffprobePath, args...)
|
||||||
|
setHideWindow(cmd)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
results[idx] = info
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
kvMap := make(map[string]string)
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
if parts := strings.SplitN(line, "=", 2); len(parts) == 2 {
|
||||||
|
kvMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := kvMap["sample_rate"]; ok {
|
||||||
|
if s, err := strconv.Atoi(v); err == nil {
|
||||||
|
info.SampleRate = uint32(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bits := 0
|
||||||
|
if v, ok := kvMap["bits_per_raw_sample"]; ok && v != "N/A" && v != "" {
|
||||||
|
bits, _ = strconv.Atoi(v)
|
||||||
|
}
|
||||||
|
if bits == 0 {
|
||||||
|
if v, ok := kvMap["bits_per_sample"]; ok && v != "N/A" && v != "" {
|
||||||
|
bits, _ = strconv.Atoi(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.BitsPerSample = uint8(bits)
|
||||||
|
|
||||||
|
results[idx] = info
|
||||||
|
}(i, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResampleRequest struct {
|
||||||
|
InputFiles []string `json:"input_files"`
|
||||||
|
SampleRate string `json:"sample_rate"`
|
||||||
|
BitDepth string `json:"bit_depth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResampleResult struct {
|
||||||
|
InputFile string `json:"input_file"`
|
||||||
|
OutputFile string `json:"output_file"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFolderLabel(sampleRate, bitDepth string) string {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
if bitDepth != "" {
|
||||||
|
parts = append(parts, bitDepth+"bit")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sampleRate {
|
||||||
|
case "44100":
|
||||||
|
parts = append(parts, "44.1kHz")
|
||||||
|
case "48000":
|
||||||
|
parts = append(parts, "48kHz")
|
||||||
|
case "96000":
|
||||||
|
parts = append(parts, "96kHz")
|
||||||
|
case "192000":
|
||||||
|
parts = append(parts, "192kHz")
|
||||||
|
default:
|
||||||
|
if sampleRate != "" {
|
||||||
|
parts = append(parts, sampleRate+"Hz")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "Resampled"
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResampleAudio(req ResampleRequest) ([]ResampleResult, error) {
|
||||||
|
ffmpegPath, err := GetFFmpegPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get ffmpeg path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateExecutable(ffmpegPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ffmpeg executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, err := IsFFmpegInstalled()
|
||||||
|
if err != nil || !installed {
|
||||||
|
return nil, fmt.Errorf("ffmpeg is not installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.SampleRate == "" && req.BitDepth == "" {
|
||||||
|
return nil, fmt.Errorf("at least one of sample rate or bit depth must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]ResampleResult, len(req.InputFiles))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
folderLabel := buildFolderLabel(req.SampleRate, req.BitDepth)
|
||||||
|
|
||||||
|
for i, inputFile := range req.InputFiles {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int, inputFile string) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
result := ResampleResult{
|
||||||
|
InputFile: inputFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
inputExt := strings.ToLower(filepath.Ext(inputFile))
|
||||||
|
baseName := strings.TrimSuffix(filepath.Base(inputFile), inputExt)
|
||||||
|
inputDir := filepath.Dir(inputFile)
|
||||||
|
|
||||||
|
outputDir := filepath.Join(inputDir, folderLabel)
|
||||||
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
|
result.Error = fmt.Sprintf("failed to create output directory: %v", err)
|
||||||
|
result.Success = false
|
||||||
|
mu.Lock()
|
||||||
|
results[idx] = result
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFile := filepath.Join(outputDir, baseName+".flac")
|
||||||
|
result.OutputFile = outputFile
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-i", inputFile,
|
||||||
|
"-y",
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.BitDepth != "" {
|
||||||
|
switch req.BitDepth {
|
||||||
|
case "16":
|
||||||
|
args = append(args, "-c:a", "flac", "-sample_fmt", "s16")
|
||||||
|
case "24":
|
||||||
|
args = append(args, "-c:a", "flac", "-sample_fmt", "s32", "-bits_per_raw_sample", "24")
|
||||||
|
default:
|
||||||
|
args = append(args, "-c:a", "flac")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = append(args, "-c:a", "flac")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.SampleRate != "" {
|
||||||
|
args = append(args, "-ar", req.SampleRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "-map_metadata", "0")
|
||||||
|
args = append(args, outputFile)
|
||||||
|
|
||||||
|
fmt.Printf("[Resample] %s -> %s\n", inputFile, outputFile)
|
||||||
|
|
||||||
|
cmd := exec.Command(ffmpegPath, args...)
|
||||||
|
setHideWindow(cmd)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Sprintf("resampling failed: %s - %s", err.Error(), string(output))
|
||||||
|
result.Success = false
|
||||||
|
mu.Lock()
|
||||||
|
results[idx] = result
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true
|
||||||
|
fmt.Printf("[Resample] Done: %s\n", outputFile)
|
||||||
|
mu.Lock()
|
||||||
|
results[idx] = result
|
||||||
|
mu.Unlock()
|
||||||
|
}(i, inputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
package backend
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Hiragana to Romaji mapping
|
|
||||||
var hiraganaToRomaji = map[rune]string{
|
|
||||||
'あ': "a", 'い': "i", 'う': "u", 'え': "e", 'お': "o",
|
|
||||||
'か': "ka", 'き': "ki", 'く': "ku", 'け': "ke", 'こ': "ko",
|
|
||||||
'さ': "sa", 'し': "shi", 'す': "su", 'せ': "se", 'そ': "so",
|
|
||||||
'た': "ta", 'ち': "chi", 'つ': "tsu", 'て': "te", 'と': "to",
|
|
||||||
'な': "na", 'に': "ni", 'ぬ': "nu", 'ね': "ne", 'の': "no",
|
|
||||||
'は': "ha", 'ひ': "hi", 'ふ': "fu", 'へ': "he", 'ほ': "ho",
|
|
||||||
'ま': "ma", 'み': "mi", 'む': "mu", 'め': "me", 'も': "mo",
|
|
||||||
'や': "ya", 'ゆ': "yu", 'よ': "yo",
|
|
||||||
'ら': "ra", 'り': "ri", 'る': "ru", 'れ': "re", 'ろ': "ro",
|
|
||||||
'わ': "wa", 'を': "wo", 'ん': "n",
|
|
||||||
// Dakuten (voiced)
|
|
||||||
'が': "ga", 'ぎ': "gi", 'ぐ': "gu", 'げ': "ge", 'ご': "go",
|
|
||||||
'ざ': "za", 'じ': "ji", 'ず': "zu", 'ぜ': "ze", 'ぞ': "zo",
|
|
||||||
'だ': "da", 'ぢ': "ji", 'づ': "zu", 'で': "de", 'ど': "do",
|
|
||||||
'ば': "ba", 'び': "bi", 'ぶ': "bu", 'べ': "be", 'ぼ': "bo",
|
|
||||||
// Handakuten (semi-voiced)
|
|
||||||
'ぱ': "pa", 'ぴ': "pi", 'ぷ': "pu", 'ぺ': "pe", 'ぽ': "po",
|
|
||||||
// Small characters
|
|
||||||
'ゃ': "ya", 'ゅ': "yu", 'ょ': "yo",
|
|
||||||
'っ': "", // Double consonant marker
|
|
||||||
'ぁ': "a", 'ぃ': "i", 'ぅ': "u", 'ぇ': "e", 'ぉ': "o",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Katakana to Romaji mapping
|
|
||||||
var katakanaToRomaji = map[rune]string{
|
|
||||||
'ア': "a", 'イ': "i", 'ウ': "u", 'エ': "e", 'オ': "o",
|
|
||||||
'カ': "ka", 'キ': "ki", 'ク': "ku", 'ケ': "ke", 'コ': "ko",
|
|
||||||
'サ': "sa", 'シ': "shi", 'ス': "su", 'セ': "se", 'ソ': "so",
|
|
||||||
'タ': "ta", 'チ': "chi", 'ツ': "tsu", 'テ': "te", 'ト': "to",
|
|
||||||
'ナ': "na", 'ニ': "ni", 'ヌ': "nu", 'ネ': "ne", 'ノ': "no",
|
|
||||||
'ハ': "ha", 'ヒ': "hi", 'フ': "fu", 'ヘ': "he", 'ホ': "ho",
|
|
||||||
'マ': "ma", 'ミ': "mi", 'ム': "mu", 'メ': "me", 'モ': "mo",
|
|
||||||
'ヤ': "ya", 'ユ': "yu", 'ヨ': "yo",
|
|
||||||
'ラ': "ra", 'リ': "ri", 'ル': "ru", 'レ': "re", 'ロ': "ro",
|
|
||||||
'ワ': "wa", 'ヲ': "wo", 'ン': "n",
|
|
||||||
// Dakuten (voiced)
|
|
||||||
'ガ': "ga", 'ギ': "gi", 'グ': "gu", 'ゲ': "ge", 'ゴ': "go",
|
|
||||||
'ザ': "za", 'ジ': "ji", 'ズ': "zu", 'ゼ': "ze", 'ゾ': "zo",
|
|
||||||
'ダ': "da", 'ヂ': "ji", 'ヅ': "zu", 'デ': "de", 'ド': "do",
|
|
||||||
'バ': "ba", 'ビ': "bi", 'ブ': "bu", 'ベ': "be", 'ボ': "bo",
|
|
||||||
// Handakuten (semi-voiced)
|
|
||||||
'パ': "pa", 'ピ': "pi", 'プ': "pu", 'ペ': "pe", 'ポ': "po",
|
|
||||||
// Small characters
|
|
||||||
'ャ': "ya", 'ュ': "yu", 'ョ': "yo",
|
|
||||||
'ッ': "", // Double consonant marker
|
|
||||||
'ァ': "a", 'ィ': "i", 'ゥ': "u", 'ェ': "e", 'ォ': "o",
|
|
||||||
// Extended katakana
|
|
||||||
'ー': "", // Long vowel mark
|
|
||||||
'ヴ': "vu",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combination mappings for きゃ, しゃ, etc.
|
|
||||||
var combinationHiragana = map[string]string{
|
|
||||||
"きゃ": "kya", "きゅ": "kyu", "きょ": "kyo",
|
|
||||||
"しゃ": "sha", "しゅ": "shu", "しょ": "sho",
|
|
||||||
"ちゃ": "cha", "ちゅ": "chu", "ちょ": "cho",
|
|
||||||
"にゃ": "nya", "にゅ": "nyu", "にょ": "nyo",
|
|
||||||
"ひゃ": "hya", "ひゅ": "hyu", "ひょ": "hyo",
|
|
||||||
"みゃ": "mya", "みゅ": "myu", "みょ": "myo",
|
|
||||||
"りゃ": "rya", "りゅ": "ryu", "りょ": "ryo",
|
|
||||||
"ぎゃ": "gya", "ぎゅ": "gyu", "ぎょ": "gyo",
|
|
||||||
"じゃ": "ja", "じゅ": "ju", "じょ": "jo",
|
|
||||||
"びゃ": "bya", "びゅ": "byu", "びょ": "byo",
|
|
||||||
"ぴゃ": "pya", "ぴゅ": "pyu", "ぴょ": "pyo",
|
|
||||||
}
|
|
||||||
|
|
||||||
var combinationKatakana = map[string]string{
|
|
||||||
"キャ": "kya", "キュ": "kyu", "キョ": "kyo",
|
|
||||||
"シャ": "sha", "シュ": "shu", "ショ": "sho",
|
|
||||||
"チャ": "cha", "チュ": "chu", "チョ": "cho",
|
|
||||||
"ニャ": "nya", "ニュ": "nyu", "ニョ": "nyo",
|
|
||||||
"ヒャ": "hya", "ヒュ": "hyu", "ヒョ": "hyo",
|
|
||||||
"ミャ": "mya", "ミュ": "myu", "ミョ": "myo",
|
|
||||||
"リャ": "rya", "リュ": "ryu", "リョ": "ryo",
|
|
||||||
"ギャ": "gya", "ギュ": "gyu", "ギョ": "gyo",
|
|
||||||
"ジャ": "ja", "ジュ": "ju", "ジョ": "jo",
|
|
||||||
"ビャ": "bya", "ビュ": "byu", "ビョ": "byo",
|
|
||||||
"ピャ": "pya", "ピュ": "pyu", "ピョ": "pyo",
|
|
||||||
// Extended combinations
|
|
||||||
"ティ": "ti", "ディ": "di", "トゥ": "tu", "ドゥ": "du",
|
|
||||||
"ファ": "fa", "フィ": "fi", "フェ": "fe", "フォ": "fo",
|
|
||||||
"ウィ": "wi", "ウェ": "we", "ウォ": "wo",
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainsJapanese checks if a string contains Japanese characters
|
|
||||||
func ContainsJapanese(s string) bool {
|
|
||||||
for _, r := range s {
|
|
||||||
if isHiragana(r) || isKatakana(r) || isKanji(r) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isHiragana(r rune) bool {
|
|
||||||
return r >= 0x3040 && r <= 0x309F
|
|
||||||
}
|
|
||||||
|
|
||||||
func isKatakana(r rune) bool {
|
|
||||||
return r >= 0x30A0 && r <= 0x30FF
|
|
||||||
}
|
|
||||||
|
|
||||||
func isKanji(r rune) bool {
|
|
||||||
return (r >= 0x4E00 && r <= 0x9FFF) || // CJK Unified Ideographs
|
|
||||||
(r >= 0x3400 && r <= 0x4DBF) // CJK Unified Ideographs Extension A
|
|
||||||
}
|
|
||||||
|
|
||||||
// JapaneseToRomaji converts Japanese text (hiragana/katakana) to romaji
|
|
||||||
// Note: Kanji cannot be converted without a dictionary, so they are kept as-is
|
|
||||||
func JapaneseToRomaji(text string) string {
|
|
||||||
if !ContainsJapanese(text) {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
var result strings.Builder
|
|
||||||
runes := []rune(text)
|
|
||||||
i := 0
|
|
||||||
|
|
||||||
for i < len(runes) {
|
|
||||||
// Check for っ/ッ (double consonant)
|
|
||||||
if i < len(runes)-1 && (runes[i] == 'っ' || runes[i] == 'ッ') {
|
|
||||||
nextRomaji := ""
|
|
||||||
if romaji, ok := hiraganaToRomaji[runes[i+1]]; ok {
|
|
||||||
nextRomaji = romaji
|
|
||||||
} else if romaji, ok := katakanaToRomaji[runes[i+1]]; ok {
|
|
||||||
nextRomaji = romaji
|
|
||||||
}
|
|
||||||
if len(nextRomaji) > 0 {
|
|
||||||
result.WriteByte(nextRomaji[0]) // Double the first consonant
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for two-character combinations
|
|
||||||
if i < len(runes)-1 {
|
|
||||||
combo := string(runes[i : i+2])
|
|
||||||
if romaji, ok := combinationHiragana[combo]; ok {
|
|
||||||
result.WriteString(romaji)
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if romaji, ok := combinationKatakana[combo]; ok {
|
|
||||||
result.WriteString(romaji)
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single character conversion
|
|
||||||
r := runes[i]
|
|
||||||
if romaji, ok := hiraganaToRomaji[r]; ok {
|
|
||||||
result.WriteString(romaji)
|
|
||||||
} else if romaji, ok := katakanaToRomaji[r]; ok {
|
|
||||||
result.WriteString(romaji)
|
|
||||||
} else if isKanji(r) {
|
|
||||||
// Keep kanji as-is (would need dictionary for proper conversion)
|
|
||||||
result.WriteRune(r)
|
|
||||||
} else {
|
|
||||||
// Keep other characters (punctuation, spaces, etc.)
|
|
||||||
result.WriteRune(r)
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildSearchQuery creates a search query from track name and artist
|
|
||||||
// Converts Japanese to romaji if present
|
|
||||||
func BuildSearchQuery(trackName, artistName string) string {
|
|
||||||
// Convert Japanese to romaji
|
|
||||||
trackRomaji := JapaneseToRomaji(trackName)
|
|
||||||
artistRomaji := JapaneseToRomaji(artistName)
|
|
||||||
|
|
||||||
// Clean up the query - remove special characters that might interfere with search
|
|
||||||
trackClean := cleanSearchQuery(trackRomaji)
|
|
||||||
artistClean := cleanSearchQuery(artistRomaji)
|
|
||||||
|
|
||||||
return strings.TrimSpace(artistClean + " " + trackClean)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanSearchQuery removes special characters that might interfere with search
|
|
||||||
func cleanSearchQuery(s string) string {
|
|
||||||
var result strings.Builder
|
|
||||||
for _, r := range s {
|
|
||||||
if unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsSpace(r) {
|
|
||||||
result.WriteRune(r)
|
|
||||||
} else if r == '-' || r == '\'' {
|
|
||||||
result.WriteRune(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(result.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanToASCII removes all non-ASCII characters and keeps only letters, numbers, spaces
|
|
||||||
// This is useful for creating search queries that work better with Tidal's search
|
|
||||||
func cleanToASCII(s string) string {
|
|
||||||
var result strings.Builder
|
|
||||||
for _, r := range s {
|
|
||||||
// Keep only ASCII letters, numbers, spaces, and basic punctuation
|
|
||||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
|
||||||
(r >= '0' && r <= '9') || r == ' ' || r == '-' || r == '\'' {
|
|
||||||
result.WriteRune(r)
|
|
||||||
} else if r == ',' || r == '.' {
|
|
||||||
// Convert punctuation to space
|
|
||||||
result.WriteRune(' ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Clean up multiple spaces
|
|
||||||
cleaned := strings.Join(strings.Fields(result.String()), " ")
|
|
||||||
return strings.TrimSpace(cleaned)
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,60 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const songLinkUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
var (
|
||||||
|
isrcPattern = regexp.MustCompile(`\b([A-Z]{2}[A-Z0-9]{3}\d{7})\b`)
|
||||||
|
amazonAlbumTrackPath = regexp.MustCompile(`/albums/[A-Z0-9]{10}/(B[0-9A-Z]{9})`)
|
||||||
|
amazonTrackPath = regexp.MustCompile(`/tracks/(B[0-9A-Z]{9})`)
|
||||||
|
)
|
||||||
|
|
||||||
type SongLinkClient struct {
|
type SongLinkClient struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
lastAPICallTime time.Time
|
|
||||||
apiCallCount int
|
|
||||||
apiCallResetTime time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SongLinkURLs struct {
|
type SongLinkURLs struct {
|
||||||
TidalURL string `json:"tidal_url"`
|
TidalURL string `json:"tidal_url"`
|
||||||
AmazonURL string `json:"amazon_url"`
|
AmazonURL string `json:"amazon_url"`
|
||||||
|
ISRC string `json:"isrc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackAvailability represents the availability of a track on different platforms
|
|
||||||
type TrackAvailability struct {
|
type TrackAvailability struct {
|
||||||
SpotifyID string `json:"spotify_id"`
|
SpotifyID string `json:"spotify_id"`
|
||||||
Tidal bool `json:"tidal"`
|
Tidal bool `json:"tidal"`
|
||||||
Amazon bool `json:"amazon"`
|
Amazon bool `json:"amazon"`
|
||||||
Qobuz bool `json:"qobuz"`
|
Qobuz bool `json:"qobuz"`
|
||||||
|
Deezer bool `json:"deezer"`
|
||||||
TidalURL string `json:"tidal_url,omitempty"`
|
TidalURL string `json:"tidal_url,omitempty"`
|
||||||
AmazonURL string `json:"amazon_url,omitempty"`
|
AmazonURL string `json:"amazon_url,omitempty"`
|
||||||
QobuzURL string `json:"qobuz_url,omitempty"`
|
QobuzURL string `json:"qobuz_url,omitempty"`
|
||||||
|
DeezerURL string `json:"deezer_url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type songLinkAPIResponse struct {
|
||||||
|
LinksByPlatform map[string]struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"linksByPlatform"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type qobuzAvailabilityTrack struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Album struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
RelativeURL string `json:"relative_url"`
|
||||||
|
} `json:"album"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSongLinkClient() *SongLinkClient {
|
func NewSongLinkClient() *SongLinkClient {
|
||||||
@@ -38,291 +62,444 @@ func NewSongLinkClient() *SongLinkClient {
|
|||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
apiCallResetTime: time.Now(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string) (*SongLinkURLs, error) {
|
func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string, region string) (*SongLinkURLs, error) {
|
||||||
// Rate limiting: max 10 requests per minute (song.link API limit)
|
links, err := s.resolveSpotifyTrackLinks(spotifyTrackID, region)
|
||||||
now := time.Now()
|
if err != nil && (links == nil || (links.TidalURL == "" && links.AmazonURL == "")) {
|
||||||
if now.Sub(s.apiCallResetTime) >= time.Minute {
|
return nil, err
|
||||||
s.apiCallCount = 0
|
|
||||||
s.apiCallResetTime = now
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've hit the limit, wait until the next minute
|
|
||||||
if s.apiCallCount >= 9 {
|
|
||||||
waitTime := time.Minute - now.Sub(s.apiCallResetTime)
|
|
||||||
if waitTime > 0 {
|
|
||||||
fmt.Printf("Rate limit reached, waiting %v...\n", waitTime.Round(time.Second))
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
s.apiCallCount = 0
|
|
||||||
s.apiCallResetTime = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add delay between requests (7 seconds to be safe)
|
|
||||||
if !s.lastAPICallTime.IsZero() {
|
|
||||||
timeSinceLastCall := now.Sub(s.lastAPICallTime)
|
|
||||||
minDelay := 7 * time.Second
|
|
||||||
if timeSinceLastCall < minDelay {
|
|
||||||
waitTime := minDelay - timeSinceLastCall
|
|
||||||
fmt.Printf("Rate limiting: waiting %v...\n", waitTime.Round(time.Second))
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 API URL
|
|
||||||
spotifyBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9vcGVuLnNwb3RpZnkuY29tL3RyYWNrLw==")
|
|
||||||
spotifyURL := fmt.Sprintf("%s%s", string(spotifyBase), spotifyTrackID)
|
|
||||||
|
|
||||||
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuc29uZy5saW5rL3YxLWFscGhhLjEvbGlua3M/dXJsPQ==")
|
|
||||||
apiURL := fmt.Sprintf("%s%s", string(apiBase), url.QueryEscape(spotifyURL))
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", apiURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Getting streaming URLs from song.link...")
|
|
||||||
|
|
||||||
// Retry logic for rate limit errors
|
|
||||||
maxRetries := 3
|
|
||||||
var resp *http.Response
|
|
||||||
for i := 0; i < maxRetries; i++ {
|
|
||||||
resp, err = s.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get URLs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update rate limit tracking
|
|
||||||
s.lastAPICallTime = time.Now()
|
|
||||||
s.apiCallCount++
|
|
||||||
|
|
||||||
if resp.StatusCode == 429 {
|
|
||||||
resp.Body.Close()
|
|
||||||
if i < maxRetries-1 {
|
|
||||||
waitTime := 15 * time.Second
|
|
||||||
fmt.Printf("Rate limited by API, waiting %v before retry...\n", waitTime)
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("API rate limit exceeded after %d retries", maxRetries)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
resp.Body.Close()
|
|
||||||
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var songLinkResp struct {
|
|
||||||
LinksByPlatform map[string]struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
} `json:"linksByPlatform"`
|
|
||||||
}
|
|
||||||
// Read body first to handle encoding issues
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(body) == 0 {
|
|
||||||
return nil, fmt.Errorf("API returned empty response")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(body, &songLinkResp); err != nil {
|
|
||||||
// Truncate body for error message (max 200 chars)
|
|
||||||
bodyStr := string(body)
|
|
||||||
if len(bodyStr) > 200 {
|
|
||||||
bodyStr = bodyStr[:200] + "..."
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
urls := &SongLinkURLs{}
|
urls := &SongLinkURLs{}
|
||||||
|
if links != nil {
|
||||||
// Extract Tidal URL
|
urls.TidalURL = links.TidalURL
|
||||||
if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" {
|
urls.AmazonURL = normalizeAmazonMusicURL(links.AmazonURL)
|
||||||
urls.TidalURL = tidalLink.URL
|
urls.ISRC = links.ISRC
|
||||||
fmt.Printf("✓ Tidal URL found\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract Amazon URL
|
|
||||||
if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" {
|
|
||||||
amazonURL := amazonLink.URL
|
|
||||||
// Convert album URL to track URL if needed
|
|
||||||
if len(amazonURL) > 0 {
|
|
||||||
urls.AmazonURL = amazonURL
|
|
||||||
fmt.Printf("✓ Amazon URL found\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if at least one URL was found
|
|
||||||
if urls.TidalURL == "" && urls.AmazonURL == "" {
|
if urls.TidalURL == "" && urls.AmazonURL == "" {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("no streaming URLs found")
|
return nil, fmt.Errorf("no streaming URLs found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls, nil
|
return urls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckTrackAvailability checks the availability of a track on different platforms
|
func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string) (*TrackAvailability, error) {
|
||||||
func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc string) (*TrackAvailability, error) {
|
links, err := s.resolveSpotifyTrackLinks(spotifyTrackID, "")
|
||||||
// Rate limiting: max 10 requests per minute (song.link API limit)
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(s.apiCallResetTime) >= time.Minute {
|
|
||||||
s.apiCallCount = 0
|
|
||||||
s.apiCallResetTime = now
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've hit the limit, wait until the next minute
|
|
||||||
if s.apiCallCount >= 9 {
|
|
||||||
waitTime := time.Minute - now.Sub(s.apiCallResetTime)
|
|
||||||
if waitTime > 0 {
|
|
||||||
fmt.Printf("Rate limit reached, waiting %v...\n", waitTime.Round(time.Second))
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
s.apiCallCount = 0
|
|
||||||
s.apiCallResetTime = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add delay between requests (7 seconds to be safe)
|
|
||||||
if !s.lastAPICallTime.IsZero() {
|
|
||||||
timeSinceLastCall := now.Sub(s.lastAPICallTime)
|
|
||||||
minDelay := 7 * time.Second
|
|
||||||
if timeSinceLastCall < minDelay {
|
|
||||||
waitTime := minDelay - timeSinceLastCall
|
|
||||||
fmt.Printf("Rate limiting: waiting %v...\n", waitTime.Round(time.Second))
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 API URL
|
|
||||||
spotifyBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9vcGVuLnNwb3RpZnkuY29tL3RyYWNrLw==")
|
|
||||||
spotifyURL := fmt.Sprintf("%s%s", string(spotifyBase), spotifyTrackID)
|
|
||||||
|
|
||||||
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuc29uZy5saW5rL3YxLWFscGhhLjEvbGlua3M/dXJsPQ==")
|
|
||||||
apiURL := fmt.Sprintf("%s%s", string(apiBase), url.QueryEscape(spotifyURL))
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", apiURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Checking availability for track: %s\n", spotifyTrackID)
|
|
||||||
|
|
||||||
// Retry logic for rate limit errors
|
|
||||||
maxRetries := 3
|
|
||||||
var resp *http.Response
|
|
||||||
for i := 0; i < maxRetries; i++ {
|
|
||||||
resp, err = s.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to check availability: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update rate limit tracking
|
|
||||||
s.lastAPICallTime = time.Now()
|
|
||||||
s.apiCallCount++
|
|
||||||
|
|
||||||
if resp.StatusCode == 429 {
|
|
||||||
resp.Body.Close()
|
|
||||||
if i < maxRetries-1 {
|
|
||||||
waitTime := 15 * time.Second
|
|
||||||
fmt.Printf("Rate limited by API, waiting %v before retry...\n", waitTime)
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("API rate limit exceeded after %d retries", maxRetries)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
resp.Body.Close()
|
|
||||||
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var songLinkResp struct {
|
|
||||||
LinksByPlatform map[string]struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
} `json:"linksByPlatform"`
|
|
||||||
}
|
|
||||||
// Read body first to handle encoding issues
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(body) == 0 {
|
|
||||||
return nil, fmt.Errorf("API returned empty response")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(body, &songLinkResp); err != nil {
|
|
||||||
// Truncate body for error message (max 200 chars)
|
|
||||||
bodyStr := string(body)
|
|
||||||
if len(bodyStr) > 200 {
|
|
||||||
bodyStr = bodyStr[:200] + "..."
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
availability := &TrackAvailability{
|
availability := &TrackAvailability{
|
||||||
SpotifyID: spotifyTrackID,
|
SpotifyID: spotifyTrackID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Tidal
|
if links != nil {
|
||||||
if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" {
|
availability.TidalURL = links.TidalURL
|
||||||
availability.Tidal = true
|
availability.AmazonURL = normalizeAmazonMusicURL(links.AmazonURL)
|
||||||
availability.TidalURL = tidalLink.URL
|
availability.DeezerURL = normalizeDeezerTrackURL(links.DeezerURL)
|
||||||
|
availability.Tidal = availability.TidalURL != ""
|
||||||
|
availability.Amazon = availability.AmazonURL != ""
|
||||||
|
availability.Deezer = availability.DeezerURL != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Amazon
|
isrc := ""
|
||||||
if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" {
|
if links != nil {
|
||||||
availability.Amazon = true
|
isrc = strings.TrimSpace(links.ISRC)
|
||||||
availability.AmazonURL = amazonLink.URL
|
}
|
||||||
|
|
||||||
|
if isrc == "" && availability.DeezerURL != "" {
|
||||||
|
if resolvedISRC, deezerErr := getDeezerISRC(availability.DeezerURL); deezerErr == nil {
|
||||||
|
isrc = resolvedISRC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isrc == "" {
|
||||||
|
if fallbackISRC, fallbackErr := s.lookupSpotifyISRC(spotifyTrackID); fallbackErr == nil {
|
||||||
|
isrc = fallbackISRC
|
||||||
|
} else if err == nil {
|
||||||
|
err = fallbackErr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Qobuz using ISRC (song.link doesn't support Qobuz)
|
|
||||||
if isrc != "" {
|
if isrc != "" {
|
||||||
qobuzAvailable := checkQobuzAvailability(isrc)
|
availability.Qobuz, availability.QobuzURL = checkQobuzAvailability(isrc)
|
||||||
availability.Qobuz = qobuzAvailable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if availability.Tidal || availability.Amazon || availability.Deezer || availability.Qobuz {
|
||||||
return availability, nil
|
return availability, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return availability, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return availability, fmt.Errorf("no platforms found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkQobuzAvailability checks if a track is available on Qobuz using ISRC
|
func qobuzNormalizeRelativeURL(rawURL string) string {
|
||||||
func checkQobuzAvailability(isrc string) bool {
|
rawURL = strings.TrimSpace(rawURL)
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
if rawURL == "" {
|
||||||
appID := "798273057"
|
return ""
|
||||||
|
|
||||||
// Decode base64 API URL
|
|
||||||
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly93d3cucW9idXouY29tL2FwaS5qc29uLzAuMi90cmFjay9zZWFyY2g/cXVlcnk9")
|
|
||||||
searchURL := fmt.Sprintf("%s%s&limit=1&app_id=%s", string(apiBase), isrc, appID)
|
|
||||||
|
|
||||||
resp, err := client.Get(searchURL)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
if strings.HasPrefix(rawURL, "http://") || strings.HasPrefix(rawURL, "https://") {
|
||||||
|
return rawURL
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(rawURL, "/") {
|
||||||
|
return "https://www.qobuz.com" + rawURL
|
||||||
|
}
|
||||||
|
return "https://www.qobuz.com/" + rawURL
|
||||||
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
func qobuzSlugifySegment(value string) string {
|
||||||
return false
|
value = strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
lastDash := false
|
||||||
|
for _, r := range value {
|
||||||
|
switch {
|
||||||
|
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
|
||||||
|
builder.WriteRune(r)
|
||||||
|
lastDash = false
|
||||||
|
default:
|
||||||
|
if !lastDash {
|
||||||
|
builder.WriteByte('-')
|
||||||
|
lastDash = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(builder.String(), "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzAlbumSlugURL(albumTitle string, albumID string) string {
|
||||||
|
albumID = strings.TrimSpace(albumID)
|
||||||
|
if albumID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
slug := qobuzSlugifySegment(albumTitle)
|
||||||
|
if slug == "" {
|
||||||
|
return fmt.Sprintf("https://www.qobuz.com/album/%s", albumID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("https://www.qobuz.com/album/%s/%s", slug, albumID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkQobuzAvailability(isrc string) (bool, string) {
|
||||||
var searchResp struct {
|
var searchResp struct {
|
||||||
Tracks struct {
|
Tracks struct {
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
|
Items []qobuzAvailabilityTrack `json:"items"`
|
||||||
} `json:"tracks"`
|
} `json:"tracks"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
|
|
||||||
return false
|
if err := doQobuzSignedJSONRequest("track/search", url.Values{
|
||||||
|
"query": {strings.TrimSpace(isrc)},
|
||||||
|
"limit": {"1"},
|
||||||
|
}, &searchResp); err != nil {
|
||||||
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchResp.Tracks.Total > 0
|
if searchResp.Tracks.Total == 0 || len(searchResp.Tracks.Items) == 0 {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
item := searchResp.Tracks.Items[0]
|
||||||
|
qobuzURL := strings.TrimSpace(item.Album.URL)
|
||||||
|
if qobuzURL == "" {
|
||||||
|
qobuzURL = qobuzNormalizeRelativeURL(item.Album.RelativeURL)
|
||||||
|
}
|
||||||
|
if qobuzURL == "" {
|
||||||
|
qobuzURL = qobuzAlbumSlugURL(item.Album.Title, item.Album.ID)
|
||||||
|
}
|
||||||
|
if qobuzURL == "" && item.ID > 0 {
|
||||||
|
qobuzURL = fmt.Sprintf("https://www.qobuz.com/us-en/track/%d", item.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, qobuzURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) GetDeezerURLFromSpotify(spotifyTrackID string) (string, error) {
|
||||||
|
links, err := s.resolveSpotifyTrackLinks(spotifyTrackID, "")
|
||||||
|
if links != nil && links.DeezerURL != "" {
|
||||||
|
deezerURL := normalizeDeezerTrackURL(links.DeezerURL)
|
||||||
|
fmt.Printf("Found Deezer URL: %s\n", deezerURL)
|
||||||
|
return deezerURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isrc := ""
|
||||||
|
if links != nil {
|
||||||
|
isrc = strings.TrimSpace(links.ISRC)
|
||||||
|
}
|
||||||
|
if isrc == "" {
|
||||||
|
fallbackISRC, lookupErr := s.lookupSpotifyISRC(spotifyTrackID)
|
||||||
|
if lookupErr == nil {
|
||||||
|
isrc = fallbackISRC
|
||||||
|
} else if err == nil {
|
||||||
|
err = lookupErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isrc != "" {
|
||||||
|
deezerURL, deezerErr := s.lookupDeezerTrackURLByISRC(isrc)
|
||||||
|
if deezerErr == nil {
|
||||||
|
fmt.Printf("Found Deezer URL: %s\n", deezerURL)
|
||||||
|
return deezerURL, nil
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = deezerErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("deezer link not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDeezerISRC(deezerURL string) (string, error) {
|
||||||
|
trackID, err := extractDeezerTrackID(deezerURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := fmt.Sprintf("https://api.deezer.com/track/%s", trackID)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Get(apiURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to call Deezer API: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("Deezer API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var deezerTrack struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ISRC string `json:"isrc"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&deezerTrack); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode Deezer API response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if deezerTrack.ISRC == "" {
|
||||||
|
return "", fmt.Errorf("ISRC not found in Deezer API response for track %s", trackID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found ISRC from Deezer: %s (track: %s)\n", deezerTrack.ISRC, deezerTrack.Title)
|
||||||
|
return strings.ToUpper(strings.TrimSpace(deezerTrack.ISRC)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) GetISRC(spotifyID string) (string, error) {
|
||||||
|
links, err := s.resolveSpotifyTrackLinks(spotifyID, "")
|
||||||
|
if links != nil && links.ISRC != "" {
|
||||||
|
return links.ISRC, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if links != nil && links.DeezerURL != "" {
|
||||||
|
if isrc, deezerErr := getDeezerISRC(links.DeezerURL); deezerErr == nil {
|
||||||
|
return isrc, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isrc, lookupErr := s.lookupSpotifyISRC(spotifyID)
|
||||||
|
if lookupErr == nil && isrc != "" {
|
||||||
|
return isrc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && lookupErr != nil {
|
||||||
|
return "", fmt.Errorf("%v | %v", err, lookupErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if lookupErr != nil {
|
||||||
|
return "", lookupErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("ISRC not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) GetISRCDirect(spotifyID string) (string, error) {
|
||||||
|
return s.lookupSpotifyISRC(spotifyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) fetchSongLinkLinksByURL(rawURL string, region string) (*songLinkAPIResponse, error) {
|
||||||
|
apiURL := fmt.Sprintf("https://api.song.link/v1-alpha.1/links?url=%s", url.QueryEscape(rawURL))
|
||||||
|
if region != "" {
|
||||||
|
apiURL += fmt.Sprintf("&userCountry=%s", url.QueryEscape(region))
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", songLinkUserAgent)
|
||||||
|
|
||||||
|
resp, err := s.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to call song.link: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyPreview, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
|
||||||
|
return nil, fmt.Errorf("song.link returned status %d (%s)", resp.StatusCode, strings.TrimSpace(string(bodyPreview)))
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read song.link response: %w", err)
|
||||||
|
}
|
||||||
|
if len(body) == 0 {
|
||||||
|
return nil, fmt.Errorf("song.link returned empty response")
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed songLinkAPIResponse
|
||||||
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||||
|
bodyStr := string(body)
|
||||||
|
if len(bodyStr) > 200 {
|
||||||
|
bodyStr = bodyStr[:200] + "..."
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to decode song.link response: %w (response: %s)", err, bodyStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &parsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) lookupDeezerTrackURLByISRC(isrc string) (string, error) {
|
||||||
|
apiURL := fmt.Sprintf("https://api.deezer.com/track/isrc:%s", strings.ToUpper(strings.TrimSpace(isrc)))
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", songLinkUserAgent)
|
||||||
|
|
||||||
|
resp, err := s.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to call Deezer ISRC API: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("Deezer ISRC API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ISRC string `json:"isrc"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode Deezer ISRC response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Link != "" {
|
||||||
|
return normalizeDeezerTrackURL(payload.Link), nil
|
||||||
|
}
|
||||||
|
if payload.ID > 0 {
|
||||||
|
return normalizeDeezerTrackURL(fmt.Sprintf("https://www.deezer.com/track/%d", payload.ID)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("deezer track link not found for ISRC %s", isrc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeSongLinkResponse(links *resolvedTrackLinks, resp *songLinkAPIResponse) {
|
||||||
|
if resp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if link, ok := resp.LinksByPlatform["tidal"]; ok && link.URL != "" && links.TidalURL == "" {
|
||||||
|
links.TidalURL = strings.TrimSpace(link.URL)
|
||||||
|
fmt.Println("Tidal URL found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if link, ok := resp.LinksByPlatform["amazonMusic"]; ok && link.URL != "" && links.AmazonURL == "" {
|
||||||
|
links.AmazonURL = normalizeAmazonMusicURL(link.URL)
|
||||||
|
fmt.Println("Amazon URL found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if link, ok := resp.LinksByPlatform["deezer"]; ok && link.URL != "" && links.DeezerURL == "" {
|
||||||
|
links.DeezerURL = normalizeDeezerTrackURL(link.URL)
|
||||||
|
fmt.Println("Deezer URL found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAmazonMusicURL(rawURL string) string {
|
||||||
|
amazonURL := strings.TrimSpace(rawURL)
|
||||||
|
if amazonURL == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(amazonURL, "trackAsin=") {
|
||||||
|
parts := strings.Split(amazonURL, "trackAsin=")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
trackAsin := strings.Split(parts[1], "&")[0]
|
||||||
|
if trackAsin != "" {
|
||||||
|
return fmt.Sprintf("https://music.amazon.com/tracks/%s?musicTerritory=US", trackAsin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := amazonAlbumTrackPath.FindStringSubmatch(amazonURL); len(match) > 1 {
|
||||||
|
return fmt.Sprintf("https://music.amazon.com/tracks/%s?musicTerritory=US", match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := amazonTrackPath.FindStringSubmatch(amazonURL); len(match) > 1 {
|
||||||
|
return fmt.Sprintf("https://music.amazon.com/tracks/%s?musicTerritory=US", match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDeezerTrackURL(rawURL string) string {
|
||||||
|
trackID, err := extractDeezerTrackID(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return strings.TrimSpace(rawURL)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("https://www.deezer.com/track/%s", trackID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractDeezerTrackID(rawURL string) (string, error) {
|
||||||
|
cleanURL := strings.TrimSpace(rawURL)
|
||||||
|
if cleanURL == "" {
|
||||||
|
return "", fmt.Errorf("empty Deezer URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(cleanURL, "/track/")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return "", fmt.Errorf("could not extract track ID from Deezer URL: %s", rawURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
trackID := strings.Split(parts[1], "?")[0]
|
||||||
|
trackID = strings.Trim(trackID, "/ ")
|
||||||
|
if trackID == "" {
|
||||||
|
return "", fmt.Errorf("could not extract track ID from Deezer URL: %s", rawURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAnySongLinkData(links *resolvedTrackLinks) bool {
|
||||||
|
if links == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return links.TidalURL != "" || links.AmazonURL != "" || links.DeezerURL != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstISRCMatch(body string) string {
|
||||||
|
match := isrcPattern.FindStringSubmatch(strings.ToUpper(body))
|
||||||
|
if len(match) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(match[1])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var songstatsScriptPattern = regexp.MustCompile(`(?is)<script[^>]+type=["']application/ld\+json["'][^>]*>(.*?)</script>`)
|
||||||
|
|
||||||
|
func (s *SongLinkClient) populateLinksFromSongstats(links *resolvedTrackLinks, isrc string) error {
|
||||||
|
pageURL := fmt.Sprintf("https://songstats.com/%s?ref=ISRCFinder", strings.ToUpper(strings.TrimSpace(isrc)))
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, pageURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", songLinkUserAgent)
|
||||||
|
|
||||||
|
resp, err := s.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch Songstats page: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("Songstats returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read Songstats response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := songstatsScriptPattern.FindAllStringSubmatch(string(body), -1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return fmt.Errorf("Songstats JSON-LD not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptBody := strings.TrimSpace(html.UnescapeString(match[1]))
|
||||||
|
if scriptBody == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload interface{}
|
||||||
|
if err := json.Unmarshal([]byte(scriptBody), &payload); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
before := *links
|
||||||
|
collectSongstatsLinks(payload, links)
|
||||||
|
if *links != before {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found && !hasAnySongLinkData(links) {
|
||||||
|
return fmt.Errorf("no platform links found in Songstats")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectSongstatsLinks(value interface{}, links *resolvedTrackLinks) {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if sameAs, ok := typed["sameAs"]; ok {
|
||||||
|
applySongstatsSameAs(sameAs, links)
|
||||||
|
}
|
||||||
|
for _, nested := range typed {
|
||||||
|
collectSongstatsLinks(nested, links)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, nested := range typed {
|
||||||
|
collectSongstatsLinks(nested, links)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySongstatsSameAs(value interface{}, links *resolvedTrackLinks) {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case string:
|
||||||
|
assignSongstatsLink(typed, links)
|
||||||
|
case []interface{}:
|
||||||
|
for _, item := range typed {
|
||||||
|
if link, ok := item.(string); ok {
|
||||||
|
assignSongstatsLink(link, links)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assignSongstatsLink(rawLink string, links *resolvedTrackLinks) {
|
||||||
|
link := strings.TrimSpace(rawLink)
|
||||||
|
if link == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(link, "listen.tidal.com/track"):
|
||||||
|
if links.TidalURL == "" {
|
||||||
|
links.TidalURL = link
|
||||||
|
fmt.Println("Tidal URL found via Songstats")
|
||||||
|
}
|
||||||
|
case strings.Contains(link, "music.amazon.com"):
|
||||||
|
if links.AmazonURL == "" {
|
||||||
|
if normalized := normalizeAmazonMusicURL(link); normalized != "" {
|
||||||
|
links.AmazonURL = normalized
|
||||||
|
fmt.Println("Amazon URL found via Songstats")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case strings.Contains(link, "deezer.com"):
|
||||||
|
if links.DeezerURL == "" {
|
||||||
|
links.DeezerURL = normalizeDeezerTrackURL(link)
|
||||||
|
fmt.Println("Deezer URL found via Songstats")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
soundplateSpotifyAPIURL = "https://phpstack-822472-6184058.cloudwaysapps.com/api/spotify.php"
|
||||||
|
soundplateRefererURL = "https://phpstack-822472-6184058.cloudwaysapps.com/?"
|
||||||
|
soundplateUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
|
|
||||||
|
type soundplateSpotifyResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Artist string `json:"artist"`
|
||||||
|
Album string `json:"album"`
|
||||||
|
AlbumType string `json:"album_type"`
|
||||||
|
ArtworkURL string `json:"artwork_url"`
|
||||||
|
ISRC string `json:"isrc"`
|
||||||
|
Year string `json:"year"`
|
||||||
|
SpotifyURL string `json:"spotify_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SongLinkClient) lookupSpotifyISRCViaSoundplate(spotifyTrackID string) (string, string, error) {
|
||||||
|
normalizedTrackID, err := extractSpotifyTrackID(spotifyTrackID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
spotifyTrackURL := fmt.Sprintf("https://open.spotify.com/track/%s", normalizedTrackID)
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("q", spotifyTrackURL)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, soundplateSpotifyAPIURL+"?"+query.Encode(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to create Soundplate ISRC request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", soundplateUserAgent)
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
req.Header.Set("Referer", soundplateRefererURL)
|
||||||
|
req.Header.Set("Accept-Language", "en-US,en;q=0.9,id;q=0.8")
|
||||||
|
req.Header.Set("Sec-CH-UA", "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"")
|
||||||
|
req.Header.Set("Sec-CH-UA-Mobile", "?0")
|
||||||
|
req.Header.Set("Sec-CH-UA-Platform", "\"Windows\"")
|
||||||
|
req.Header.Set("Sec-Fetch-Dest", "empty")
|
||||||
|
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||||
|
req.Header.Set("Sec-Fetch-Site", "same-origin")
|
||||||
|
req.Header.Set("Priority", "u=1, i")
|
||||||
|
|
||||||
|
resp, err := s.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("Soundplate ISRC request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to read Soundplate ISRC response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyPreview := strings.TrimSpace(string(body))
|
||||||
|
if len(bodyPreview) > 256 {
|
||||||
|
bodyPreview = bodyPreview[:256]
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("Soundplate ISRC returned status %d (%s)", resp.StatusCode, bodyPreview)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload soundplateSpotifyResponse
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to decode Soundplate ISRC response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isrc := firstISRCMatch(payload.ISRC)
|
||||||
|
if isrc == "" {
|
||||||
|
isrc = firstISRCMatch(string(body))
|
||||||
|
}
|
||||||
|
if isrc == "" {
|
||||||
|
return "", "", fmt.Errorf("ISRC missing in Soundplate response")
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedTrackID := ""
|
||||||
|
if payload.SpotifyURL != "" {
|
||||||
|
if trackID, err := extractSpotifyTrackID(payload.SpotifyURL); err == nil {
|
||||||
|
resolvedTrackID = trackID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isrc, resolvedTrackID, nil
|
||||||
|
}
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
package backend
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"math/cmplx"
|
|
||||||
|
|
||||||
"github.com/mewkiz/flac"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SpectrumData contains frequency spectrum information
|
|
||||||
type SpectrumData struct {
|
|
||||||
TimeSlices []TimeSlice `json:"time_slices"`
|
|
||||||
SampleRate int `json:"sample_rate"`
|
|
||||||
FreqBins int `json:"freq_bins"`
|
|
||||||
Duration float64 `json:"duration"`
|
|
||||||
MaxFreq float64 `json:"max_freq"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeSlice represents spectrum data at a point in time
|
|
||||||
type TimeSlice struct {
|
|
||||||
Time float64 `json:"time"`
|
|
||||||
Magnitudes []float64 `json:"magnitudes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnalyzeSpectrum decodes FLAC file and performs FFT analysis
|
|
||||||
func AnalyzeSpectrum(filepath string) (*SpectrumData, error) {
|
|
||||||
// Open FLAC file
|
|
||||||
stream, err := flac.ParseFile(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse FLAC: %w", err)
|
|
||||||
}
|
|
||||||
defer stream.Close()
|
|
||||||
|
|
||||||
info := stream.Info
|
|
||||||
sampleRate := int(info.SampleRate)
|
|
||||||
channels := int(info.NChannels)
|
|
||||||
|
|
||||||
// Read audio samples
|
|
||||||
samples, err := readSamples(stream, channels)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read samples: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(samples) == 0 {
|
|
||||||
return nil, fmt.Errorf("no audio samples found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate spectrum
|
|
||||||
return calculateSpectrum(samples, sampleRate), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readSamples reads and decodes audio samples from FLAC stream
|
|
||||||
func readSamples(stream *flac.Stream, channels int) ([]float64, error) {
|
|
||||||
var allSamples []float64
|
|
||||||
maxSamples := 10 * 1024 * 1024 // Limit to ~10 million samples to avoid memory issues
|
|
||||||
|
|
||||||
// Decode frames
|
|
||||||
for {
|
|
||||||
frame, err := stream.ParseNext()
|
|
||||||
if err != nil {
|
|
||||||
// End of stream
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert samples to float64 and mix channels to mono
|
|
||||||
for i := 0; i < frame.Subframes[0].NSamples; i++ {
|
|
||||||
var sample float64
|
|
||||||
|
|
||||||
// Mix all channels to mono by averaging
|
|
||||||
for ch := 0; ch < channels; ch++ {
|
|
||||||
sample += float64(frame.Subframes[ch].Samples[i])
|
|
||||||
}
|
|
||||||
sample /= float64(channels)
|
|
||||||
|
|
||||||
allSamples = append(allSamples, sample)
|
|
||||||
|
|
||||||
// Limit sample count
|
|
||||||
if len(allSamples) >= maxSamples {
|
|
||||||
return allSamples, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allSamples, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculateSpectrum performs FFT analysis on audio samples
|
|
||||||
func calculateSpectrum(samples []float64, sampleRate int) *SpectrumData {
|
|
||||||
fftSize := 8192
|
|
||||||
numTimeSlices := 300
|
|
||||||
|
|
||||||
duration := float64(len(samples)) / float64(sampleRate)
|
|
||||||
|
|
||||||
samplesPerSlice := len(samples) / numTimeSlices
|
|
||||||
if samplesPerSlice < fftSize {
|
|
||||||
samplesPerSlice = fftSize
|
|
||||||
numTimeSlices = len(samples) / fftSize
|
|
||||||
}
|
|
||||||
|
|
||||||
timeSlices := make([]TimeSlice, 0, numTimeSlices)
|
|
||||||
freqBins := fftSize / 2
|
|
||||||
maxFreq := float64(sampleRate) / 2.0
|
|
||||||
|
|
||||||
for i := 0; i < numTimeSlices; i++ {
|
|
||||||
startIdx := i * samplesPerSlice
|
|
||||||
if startIdx+fftSize > len(samples) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
window := samples[startIdx : startIdx+fftSize]
|
|
||||||
|
|
||||||
windowedSamples := applyHannWindow(window)
|
|
||||||
|
|
||||||
spectrum := fft(windowedSamples)
|
|
||||||
|
|
||||||
magnitudes := make([]float64, freqBins)
|
|
||||||
for j := 0; j < freqBins; j++ {
|
|
||||||
magnitude := cmplx.Abs(spectrum[j])
|
|
||||||
|
|
||||||
if magnitude < 1e-10 {
|
|
||||||
magnitude = 1e-10
|
|
||||||
}
|
|
||||||
magnitudes[j] = 20 * math.Log10(magnitude)
|
|
||||||
}
|
|
||||||
|
|
||||||
timeSlice := TimeSlice{
|
|
||||||
Time: float64(startIdx) / float64(sampleRate),
|
|
||||||
Magnitudes: magnitudes,
|
|
||||||
}
|
|
||||||
timeSlices = append(timeSlices, timeSlice)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SpectrumData{
|
|
||||||
TimeSlices: timeSlices,
|
|
||||||
SampleRate: sampleRate,
|
|
||||||
FreqBins: freqBins,
|
|
||||||
Duration: duration,
|
|
||||||
MaxFreq: maxFreq,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyHannWindow applies Hann window to reduce spectral leakage
|
|
||||||
func applyHannWindow(samples []float64) []float64 {
|
|
||||||
n := len(samples)
|
|
||||||
windowed := make([]float64, n)
|
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
window := 0.5 * (1.0 - math.Cos(2.0*math.Pi*float64(i)/float64(n-1)))
|
|
||||||
windowed[i] = samples[i] * window
|
|
||||||
}
|
|
||||||
|
|
||||||
return windowed
|
|
||||||
}
|
|
||||||
|
|
||||||
// fft performs Fast Fourier Transform using Cooley-Tukey algorithm
|
|
||||||
func fft(samples []float64) []complex128 {
|
|
||||||
n := len(samples)
|
|
||||||
|
|
||||||
x := make([]complex128, n)
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
x[i] = complex(samples[i], 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fftRecursive(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fftRecursive performs recursive FFT
|
|
||||||
func fftRecursive(x []complex128) []complex128 {
|
|
||||||
n := len(x)
|
|
||||||
|
|
||||||
if n <= 1 {
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
even := make([]complex128, n/2)
|
|
||||||
odd := make([]complex128, n/2)
|
|
||||||
|
|
||||||
for i := 0; i < n/2; i++ {
|
|
||||||
even[i] = x[2*i]
|
|
||||||
odd[i] = x[2*i+1]
|
|
||||||
}
|
|
||||||
|
|
||||||
evenFFT := fftRecursive(even)
|
|
||||||
oddFFT := fftRecursive(odd)
|
|
||||||
|
|
||||||
result := make([]complex128, n)
|
|
||||||
for k := 0; k < n/2; k++ {
|
|
||||||
t := cmplx.Exp(complex(0, -2*math.Pi*float64(k)/float64(n))) * oddFFT[k]
|
|
||||||
result[k] = evenFFT[k] + t
|
|
||||||
result[k+n/2] = evenFFT[k] - t
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
spotifyTOTPSecret = "GM3TMMJTGYZTQNZVGM4DINJZHA4TGOBYGMZTCMRTGEYDSMJRHE4TEOBUG4YTCMRUGQ4DQOJUGQYTAMRRGA2TCMJSHE3TCMBY"
|
||||||
|
spotifyTOTPVersion = 61
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateSpotifyTOTP(now time.Time) (string, int, error) {
|
||||||
|
key, err := otp.NewKeyFromURL(fmt.Sprintf("otpauth://totp/secret?secret=%s", spotifyTOTPSecret))
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := totp.GenerateCode(key.Secret(), now)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, spotifyTOTPVersion, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tidalCommunityResponse struct {
|
||||||
|
Quality string `json:"quality"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Lyric string `json:"lyric"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var tidalCommunityClient = &http.Client{Timeout: 60 * time.Second}
|
||||||
|
|
||||||
|
func mapTidalQualityToCommunity(quality string) string {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(quality)) {
|
||||||
|
case "HI_RES_LOSSLESS", "HI_RES", "24":
|
||||||
|
return "24"
|
||||||
|
default:
|
||||||
|
return "16"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TidalDownloader) getTidalCommunityDownloadURL(trackID int64, quality string) (string, error) {
|
||||||
|
payload, err := json.Marshal(map[string]string{
|
||||||
|
"id": fmt.Sprintf("%d", trackID),
|
||||||
|
"quality": mapTidalQualityToCommunity(quality),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := doCommunityRequest(tidalCommunityClient, "Tidal", func() (*http.Request, error) {
|
||||||
|
req, err := NewRequestWithDefaultHeaders(http.MethodPost, GetTidalCommunityDownloadURL(), bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if err := setCommunityRequestHeaders(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Tidal community request failed: %v\n", err)
|
||||||
|
return "", fmt.Errorf("failed to get download URL: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
preview := string(body)
|
||||||
|
if len(preview) > 200 {
|
||||||
|
preview = preview[:200]
|
||||||
|
}
|
||||||
|
fmt.Printf("Tidal community API status %d: %s\n", resp.StatusCode, preview)
|
||||||
|
return "", fmt.Errorf("tidal community API returned status %d: %s", resp.StatusCode, preview)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed tidalCommunityResponse
|
||||||
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode tidal community response: %w", err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(parsed.URL) == "" {
|
||||||
|
return "", fmt.Errorf("no download URL in tidal community response")
|
||||||
|
}
|
||||||
|
fmt.Printf("Tidal community URL found (quality %s)\n", parsed.Quality)
|
||||||
|
return parsed.URL, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const preferredUPCTagKey = "UPC"
|
||||||
|
|
||||||
|
var ffprobeUPCTagKeys = []string{
|
||||||
|
"upc",
|
||||||
|
"barcode",
|
||||||
|
"wm/upc",
|
||||||
|
"txxx:upc",
|
||||||
|
"txxx:barcode",
|
||||||
|
"txxx/upc",
|
||||||
|
"txxx/barcode",
|
||||||
|
"----:com.apple.itunes:upc",
|
||||||
|
"----:com.apple.itunes:barcode",
|
||||||
|
}
|
||||||
|
|
||||||
|
func assignPreferredUPC(current *string, incoming string, preferred bool) {
|
||||||
|
incoming = strings.TrimSpace(incoming)
|
||||||
|
if incoming == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if preferred || strings.TrimSpace(*current) == "" {
|
||||||
|
*current = incoming
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifyUPCDescription(description string) (matched bool, preferred bool) {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(description)) {
|
||||||
|
case preferredUPCTagKey:
|
||||||
|
return true, true
|
||||||
|
case "BARCODE":
|
||||||
|
return true, false
|
||||||
|
default:
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstPreferredFFprobeUPCValue(tags map[string]string) string {
|
||||||
|
for _, key := range ffprobeUPCTagKeys {
|
||||||
|
value := strings.TrimSpace(tags[key])
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import js from '@eslint/js'
|
import js from '@eslint/js';
|
||||||
import globals from 'globals'
|
import globals from 'globals';
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint';
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist']),
|
globalIgnores(['dist']),
|
||||||
{
|
{
|
||||||
@@ -20,4 +19,4 @@ export default defineConfig([
|
|||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300..800&family=Figtree:wght@300..900&family=Geist:wght@100..900&family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Google+Sans+Flex:opsz,wght@6..144,1..1000&family=Inter:wght@300..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Manrope:wght@300..800&family=Noto+Sans:wght@100..900&family=Nunito+Sans:opsz,wght@6..12,200..1000&family=Outfit:wght@100..900&family=Plus+Jakarta+Sans:wght@300..800&family=Poppins:wght@300;400;500;600;700;800&family=Public+Sans:ital,wght@0,100..900;1,100..900&family=Raleway:wght@100..900&family=Roboto:wght@300;400;500;700;900&family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&family=DM+Sans:wght@300..800&family=Figtree:wght@300..900&family=Geist:wght@100..900&family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&family=Inter:wght@300..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Manrope:wght@300..800&family=Noto+Sans:wght@100..900&family=Nunito+Sans:opsz,wght@6..12,200..1000&family=Outfit:wght@100..900&family=Plus+Jakarta+Sans:wght@300..800&family=Poppins:wght@300;400;500;600;700;800&family=Public+Sans:ital,wght@0,100..900;1,100..900&family=Raleway:wght@100..900&family=Roboto:wght@300;400;500;700;900&family=Space+Grotesk:wght@300..700&display=swap"
|
||||||
|
rel="stylesheet">
|
||||||
<title>SpotiFLAC</title>
|
<title>SpotiFLAC</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -16,39 +16,44 @@
|
|||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.575.0",
|
||||||
"motion": "^12.12.1",
|
"motion": "^12.34.3",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react-dom": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.1.18"
|
"tailwindcss": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.3.0",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^10.0.2",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^16.5.0",
|
"globals": "^17.3.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.50.0",
|
"typescript-eslint": "^8.56.1",
|
||||||
"vite": "^7.3.0"
|
"vite": "^7.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
c94dda3302d3338d7909ef5d634d0fde
|
8864b4f7b7971b624d1ba25030f2db4e
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-ad" viewBox="0 0 640 480">
|
||||||
|
<path fill="#d0103a" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fedf00" d="M0 0h435.2v480H0z"/>
|
||||||
|
<path fill="#0018a8" d="M0 0h204.8v480H0z"/>
|
||||||
|
<path fill="#c7b37f" d="M300.4 136.6c7.7 0 10.9 6.6 18.6 6.6 4.7 0 7.5-1.5 11.7-3.9 2.9-1.6 4.7-2.5 8-2.5 3.4 0 5.5 1 7.3 4 1 1.6 1.8 4.9 1.3 6.7a40 40 0 0 1-2.7 8.3c-.7 1.6-1.3 2.5-1.3 4.2 0 4.1 5.6 5.5 9.4 5.6.8 0 7.7 0 12-4.2-2.3-.1-4.9-2-4.9-4.3 0-2.6 1.8-4.3 4.3-5.1.5-.1 1.3.3 1.7 0 .7-.3.4-1 1-1.4 1.2-1 2-1.6 3.6-1.6q1.3-.1 2.5.7.5.7 1 .8c1.2 0 1.8-.8 3-.8a5 5 0 0 1 2.3.6c.6.3.6 1.5 1.4 1.5.4 0 2.4-.9 3.5-.9 2.2 0 3.4.8 4.8 2.5.4.5.6 1.4 1 1.4a6 6 0 0 1 4.8 3c.3.4.7 1.4 1.1 1.5.6.3 1 .2 1.7.7a6 6 0 0 1 2.8 4.8q-.1 1.2-.5 2.2c-1.8 6.5-6.3 8.6-10.8 14.3-2 2.4-3.5 4.3-3.5 7.4 0 .7 1 2.1 1.3 2.7-.2-1.4.5-3.2 2-3.3a4 4 0 0 1 4 3.6l-.3 1.8a10 10 0 0 1 4-1.4h1.9c3.3 0 7 1.9 9.3 3.8a21 21 0 0 1 7.3 16.8c-.8 5.2-.3 14.8-13.8 18.6 2.5 1 4.2 3 4.2 5.2a4.5 4.5 0 0 1-4.4 4.7 4 4 0 0 1-3.5-1.4c-2.8 2.8-3.3 5.7-3.3 9.7 0 2.4.4 3.8 1.4 6s1.8 3.5 3.7 5.1q1.3-2.4 4-2.6 2.7-.1 3.9 2.2c.2.5 0 .9.3 1.4.3.6.8.7 1.1 1.3.5 1 0 1.8.5 2.7.3.7.9.8 1.2 1.4.4 1 .5 1.6.5 2.7 0 3-2.7 5.2-5.7 5.2-1 0-1.4-.4-2.3-.3 1.7 1.7 3 2.5 4.3 4.5a18 18 0 0 1 3 10.3 22 22 0 0 1-2.8 11.2 20 20 0 0 1-7 8.5 35 35 0 0 1-16 6.4 74 74 0 0 1-11 1.4l-14.1.8c-7.2.4-12.2 1.5-17.3 6.6 2.4 1.7 4 3.5 4 6.4q-.2 4.7-4.7 6.2c-.7.2-1.2 0-1.9.4s-.7 1.3-1.4 1.7a6 6 0 0 1-3.8 1 8 8 0 0 1-6.4-2.5c-2.2 1.8-3 3.4-5.5 4.9-.8.4-1.2 1-2.1 1-1.5 0-2.2-1-3.4-1.8a23 23 0 0 1-4.4-4c-2.3 1.3-3.6 2.4-6.3 2.4a7 7 0 0 1-4-1c-.6-.5-.8-1.2-1.5-1.6s-1.3-.3-2.1-.7c-3-1.3-5-3.5-5-6.8 0-2.9 1.8-4.7 4.4-6-5-5-10-5.8-17-6.2l-14-.8c-4.4-.3-6.8-.7-11-1.4-3.3-.5-5.2-.7-8.2-2.1-10.2-4.8-16.8-11.3-18-22.5-.2-1-.2-1.5-.2-2.5 0-5.8 2.3-9.4 6.4-13.5-1-.3-1.7 0-2.8-.3-2.5-1-4.4-2.7-4.4-5.5q-.1-1.4.5-2.6c.4-.6 1-.7 1.2-1.4.2-1 0-1.6.4-2.5.3-.5.8-.6 1-1.2 1-1.9 2-3.4 4.1-3.4q2.7.1 3.8 2.5c1.8-.8 2.2-2.1 3.2-3.7a16 16 0 0 0 1.4-13.3c-.4-1.5-.6-2.5-1.8-3.7q-1.4 1.4-3.4 1.4c-2.9 0-5-2.5-5-5.3a5 5 0 0 1 3-4.6c-1.6-1.4-3-1.5-4.7-2.6-2.6-1.6-3.5-3.4-5.2-6-1.2-1.6-1.5-2.8-2-4.7a19 19 0 0 1-1-7.8c.6-5 1.5-8 4.6-11.9 1.8-2.3 3-3.7 5.8-4.9 2.3-1 3.7-1.7 6.2-1.7l2 .1a7 7 0 0 1 2.8.8c.4.2 1.1.9 1.1.4s-.3-.8-.3-1.3c0-2 1.5-4 3.6-4 1.5 0 2.1 1.4 2.9 2.7q.6-1 .7-2.3c0-3.4-1.9-5.2-4-7.9-4.7-5.8-10.5-8.5-10.5-16q0-3.2 3-4.9c.5-.3 1.3 0 1.8-.3s.4-1 .7-1.4q.7-.9 1.6-1.6c1-1 2-.6 3.1-1.5q.8-.7 1.2-1.4c1.3-1.6 2.5-2.4 4.6-2.4q1.3-.1 2.5.4l1 .5q.5-.5 1.5-1.1a4 4 0 0 1 2.2-.6c1.1 0 1.8.6 3 .6l.8-.6c1-.7 1.5-1 2.7-1s1.8.3 2.8 1c1 .5 1 1.3 2 1.8l1.5.4c2.6.9 4.5 2.6 4.5 5.3q.1 2.1-1.4 3.5c-.9.7-1.7.6-2.8 1a16 16 0 0 0 11.3 3.5c4.2 0 9.3-1.7 9.3-5.9 0-2-1-3-1.8-4.8a19 19 0 0 1-2.1-8.5c0-2.8.3-4.5 1.9-6.7s3.6-2.9 6.5-2.9"/>
|
||||||
|
<g fill="none" stroke="#703d29">
|
||||||
|
<path stroke-linejoin="round" stroke-width=".7" d="M272.4 159a4 4 0 0 0 2.4 2.4c.8.3 2.7.2 3.8-1.4 1-1.2 1-2.8.6-4a5 5 0 0 0-1.7-2.2z"/>
|
||||||
|
<path stroke-linecap="round" stroke-width=".7" d="M401 236.1c-1.2-2.9-4.3-1.6-4.4 0-.5 3.7 2.7 4.8 5 4.2a4 4 0 0 0 2.5-2q1-1.6.4-3.7l-.8-1.6-1.3-1.2q-1.2-.7-3.4-.6c-5.5 0-10.4 6.5-12 13.4-.6 2.2-1.3 7.3-.3 12a22 22 0 0 0 5.9 11.3 26 26 0 0 0 9.9 5.8 8 8 0 0 0 4 .1c3.2-.7 4.7-3.8 3-7-1.3-2.5-5.3-4-7.2-.6q-.3.5-.4 1.5c0 .9.4 2 1 2.4 1.5.9 3.8.6 3.7-2"/>
|
||||||
|
<path stroke-width=".8" d="M383.8 274a11 11 0 0 1 6.6-3.7q4.4-.4 8.2 2a19 19 0 0 1 10.8 17c0 3.6-1 7.5-2 9.4-.8 1.7-3 9-15.3 14-7.1 3-18 3.6-25.7 4-10.4.3-20 .7-25.5 7.6"/>
|
||||||
|
<g stroke-width=".7">
|
||||||
|
<path d="M386.4 285.7q-.4-1.5.8-3.3c1.2-1.6 3.7-2.1 6-1a7 7 0 0 1 2.5 2.2l1.1 1.6q1 1.7 1 2.5c2.5 7-1.4 14.5-6.5 17.6-4 2.4-8.7 3.4-14.4 4-2.5.4-4 .3-6.5.5h-16.8c-2.9.3-5 .4-7.6.8q-2.4.3-5.4 1-.9 0-1.8.4l-1.2.3q-5.5 1.6-9.8 4.2-1.3.7-2.5 1.7l-1.3 1.2c-2 2-3.9 4-4.4 6.7v1.6c0 1.8 1.4 4.3 5.4 5m5.5-170c.8 1.4 1.3 2.3.8 3.9q-.9 2.7-3.6 2.8c-4 0-6.3-4.8-4.5-7.8 3.2-5.3 9.3-2.3 15 .3-.3-1.3-.8-1.8-.7-3.5.1-4.2 3.2-6 4.5-10 .7-2.3 1-4.3-.7-6q-2.2-1.8-5.1-.6c-3.8 1.5-8.5 5.9-16.6 6-8.2-.1-12.8-4.5-16.7-6q-3-1.2-5.1.7c-1.7 1.6-1.4 3.6-.7 6 1.3 3.8 4.4 5.7 4.5 10 0 1.6-.4 2-.7 3.4 5.7-2.6 12-5.9 15-.3 1.7 3.2-.5 7.7-4.5 7.7q-2.7 0-3.6-2.7-.5-2.2.8-4"/>
|
||||||
|
<path stroke-linecap="round" d="M314.6 159.9a5 5 0 0 1 2.4 5c-.2 2.5-.8 3.1-2.8 4.5m2.4-3.8q0 2.2-2.3 3.1"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#c7b37f" stroke="none" d="m276.7 153.3.7.5.8.8.5 1 .2.8v1.9l-.2.8-.5.6-.6.6-.9.5-1 .2-1 .2-1-.5-.9-.6-.5-.8-.4-1v-.4z"/>
|
||||||
|
<path stroke-linecap="round" stroke-width=".7" d="M275.2 157.2c-.3-1.7-2.2-2-3-1-1.1 1.5-.3 4 2 4.7a4 4 0 0 0 3.9-1.4c1-1.3.9-2.8.5-4a5 5 0 0 0-1.7-2.2c-2.7-2-7.1-1.6-8.6 2-1.8 4.4 2.2 7.8 6 10.3 4.6 3.2 10 3.8 14 3.8 9.2-.1 16.2-4.5 20.7-7q1.7-.9 2.7.2a2 2 0 0 1-.3 2.7"/>
|
||||||
|
<path stroke-width=".7" d="m248 281.2-2 .7-2 1.6-1 1.3-1.1 2-.5 1.5-.4 1.8-.2 1.4m19-10.1-.1 1.8-.3 1.2-1 2.2-1.3 1.8-1.5 1.2-1.1.5-1.6.4"/>
|
||||||
|
<path stroke-width=".8" d="M319.7 329.1c-.3 1.7-1.9 3.6-5.3 4.2l-.6.2"/>
|
||||||
|
<path stroke-width=".9" d="M404.2 276.2a18 18 0 0 1 5.6 13.5c0 3.6-1 7.5-2 9.4-.8 1.7-3 9-15.3 14a85 85 0 0 1-25.6 4c-10.3.3-19.8.7-25.4 7.3"/>
|
||||||
|
<path stroke-width=".6" d="M387.5 282.9c.8-1 3.5-2.4 5.8-1.1a6 6 0 0 1 2.3 2"/>
|
||||||
|
<path stroke-width=".9" d="m401.6 273.8 1.4.5a7 7 0 0 0 4 0c2.8-.8 4.6-3.4 3.2-6.9a6 6 0 0 0-1.8-2.1"/>
|
||||||
|
<path stroke-linecap="round" stroke-width=".7" d="M240.3 199.8c-2 1.1-3.3 1.4-4.8 3.1a28 28 0 0 0-2.6 6.8m46-51.7q-.1 2.7-3 3.2"/>
|
||||||
|
<path stroke-width=".6" d="M397.1 192a19 19 0 0 1 18.6 19.8c0 16-9.9 18.5-13.8 19.6"/>
|
||||||
|
<path stroke-width=".7" d="M398.4 192c8.1-.3 16.5 5.7 16.9 20.7.3 11.7-8 17-12 18"/>
|
||||||
|
<path stroke-width=".6" d="m393.8 248.4.1-1.6.6-2.5.7-2 .9-1.6 1-1.3m7.8-3.4v1.5l-.5 1-.7 1.1-.8.6-1.2.5h-1.1l-.8-.1m-14.3-52.8.3-1.7.8-1.6 1-1.5 1.6-2.2 1.4-1.4 2-2.2 2-1.9 1.1-1.3 1.5-1.9 1.4-2 .8-1.7.5-2.2.1-2.7-.2-.8m-12.3 128.2 1.6-.4 1.2-.6.7-.7.5-.8.3-1.2v-.9m-158.2-12.1h2.7l1.6-.6m5-36.5-.2 1.4-.4.6-.4.6-.7.5-.7.3-1 .1h-.6m9.9-15.5-.3 2.1-.5 1-.8 1.2-1.2.9-1.2.6-2.3.5m15.3-39.7-.5 1.3-.5 1-.8 1-1 1-1.2.5-1.1.3-.6-.1m.3-6.2v1"/>
|
||||||
|
<g stroke-width=".6">
|
||||||
|
<path stroke-linecap="round" d="M254.3 224a7 7 0 0 1-2.1 1.4m150.5 44.8.5.2c1.4.8 4.2-.2 3.4-2.4"/>
|
||||||
|
<path d="M397.8 239.6c1 1.3 2.9 1.7 4.4 1.3a4 4 0 0 0 2.5-2q1-1.6.4-3.7l-.9-1.6-1.3-1.5-.4-.2m6.4 34 .1-.7a4 4 0 0 0-1.3-3l-.8-.8m.4.5c0-1.8-1.5-3.2-3.4-3.5m-4.2 2.8-1.3-1a16 16 0 0 1-4.3-10.7c0-4.2 1.6-8.4 3.6-10M341.2 324l1.8-1.6 1.2-1 2.3-1.4 2.2-1 1.6-.5 3-.6 3.6-.6m-29.5 19.4a17 17 0 0 1-7.6 6.1 18 18 0 0 1-7.6-6.1"/>
|
||||||
|
<path stroke-linecap="round" d="M314.4 332.6a10 10 0 0 1-2.2 4.2"/>
|
||||||
|
<path d="m314.7 330.5-.4 2.2M312 337l-1 1-1.7.9-2 .6m-5.6-177.8c.3-.8.5-1.4.5-2.6-.1-4.2-3.2-6.1-4.5-10-.7-2.3-1-4.3.7-6q2.2-2 5-.6c4 1.5 8.6 5.8 16.7 6-8.1-.2-12.8-4.5-16.6-6-2-.8-3.8-1-5.3.5-1.7 1.6-1.2 3.8-.5 6.1 1.3 3.9 4.2 5.8 4.3 10q-.1 1.6-.5 2.6M320 148c8-.4 14.9-5.8 17.1-6.3 2-.4 3-.2 4.5 1.1-1.4-1.3-3-1.2-5-.5-3.8 1.5-8.4 5.8-16.6 6m79.6 112.9a16 16 0 0 1-6.2-12.4c0-4.1 1.7-8.4 3.6-10m-70 97.6c-1.3 2-4.3 5-7.6 6.2a18 18 0 0 1-7.6-6.2"/>
|
||||||
|
<path stroke-linecap="round" d="m306.7 163.7 2.3-1.3c1-.6 2.3-.5 2.9.2s.7 2-.2 2.8"/>
|
||||||
|
<path d="M294.7 169.3c5.5-1.2 10-3.6 13.4-5.5M340.3 328c.5.3.8 1 .8 1l.3.8c.3 1.5-.7 2.4-2 2.6-1.7.2-3-.8-3.5-2M294.4 169c5.5-1.1 10-3.6 13.4-5.5m97.6 106.9c-1 .4-1.6.3-3-.2l-1.8-1a21 21 0 0 1-8.4-9 19 19 0 0 1-1.7-4.6 12 12 0 0 1-.5-3.3 26 26 0 0 1 4.7-15.3c1.1-1.6 2.1-2.5 4.2-2.6m-143.7-39.3a7 7 0 0 1 2.7 5.7c0 3.1-2.6 8.2-9 10a8 8 0 0 1-6.3-.8"/>
|
||||||
|
<path d="M256.3 205.6q1.6 1 1.6 3.3 0 1.6-1.9 3.7a12 12 0 0 1-8.8 4q-3 .1-6-1.7a9 9 0 0 1-3.8-5.4"/>
|
||||||
|
<path d="M256.2 212.3q1.8 1.8 1.7 4.6.1 3.9-3.7 7-.8.8-2 1.5m129.5-22.1v3.5m-.3-4.4v5m.3-15.8v6.6m-.3-8v8.9m-1.9 82a19 19 0 0 1-4.2 5.6 20 20 0 0 1-5.8 4.1 25 25 0 0 1-6.6 2.2 33 33 0 0 1-6.8.9c-2.5 0-3.9 0-6.4-.2s-4-.6-6.7-.8c-2.2-.2-3.4-.4-5.6-.3a28 28 0 0 0-11 1.8c-2.6 1-5.7 3-6.3 3.8a22 22 0 0 0-6.4-3.8 22 22 0 0 0-5.1-1.4c-2.3-.4-3.5-.4-5.8-.4s-3.4.1-5.6.3c-2.6.3-4 .6-6.7.8-2.5.2-3.9.3-6.4.2a33 33 0 0 1-13.4-3 20 20 0 0 1-6.4-4.8m42.1 53.4 1.8-.2m30.3-2.4 1.8-.1 1.7-.7 1.2-.8 1.7-2 .3-.6.3-1.7v-.8m47-136.7c.7-2.6-.2-5.4-2.8-5.3m-132 46.5a8 8 0 0 1-3.5 4.7m3.6-46.7a7 7 0 0 1-3.6 4c-1.9.8-4 0-5.2-.8"/>
|
||||||
|
<path stroke-linecap="round" d="M243.8 202.4c1.5.8 3.1-.4 2.8-2.4a3 3 0 0 0-2.5-2.2"/>
|
||||||
|
<path d="M250.2 286.6q.3.5.8.8c.7.2 1.2.4 1.9-.5.8-1.1.3-2.8-.5-3.9a5 5 0 0 0-5.8-1q-1.2.6-2.6 2.2l-1.1 1.6q-1 1.7-1.1 2.4c-2 5.9.4 12 4.1 15.7"/>
|
||||||
|
<path stroke-linecap="round" d="m340.2 327.8.7.8.2.9c.3 1.5-.7 2.4-2 2.6-1.6.2-2.8-.8-3.3-2"/>
|
||||||
|
<path d="M389.4 154.8a7.4 7.4 0 0 1 6.3 7c0 4.4-1.5 6-3.8 9.2-2.5 3.4-10.7 9.6-10.7 16.7q-.2 6.4 4.3 8.4c2 1 4.3 0 5.4-1 2.6-2.4 1.5-6.5-1.2-7-3.2-.6-3.9 4.6-.7 4.3m17.9 69a4 4 0 0 0-3.6-3 3.7 3.7 0 0 0-3.7 3.7q0 1.6 1 2.6"/>
|
||||||
|
<path d="M383.9 195.1a7 7 0 0 0-2.7 5.7c0 3.1 2.6 8.2 9 10 2.4.7 4.8.6 6.2-.3m-156-10.3a9 9 0 0 0-4.8 3.5 17 17 0 0 0-2.2 12.7 16 16 0 0 0 2.3 5.6l1 1.2 1.2 1m64 92c4.9 2.1 8.4 3.7 11.4 8.5a10 10 0 0 1 1.2 4.9c0 2.7-1 5.7-3.3 7.6a8 8 0 0 1-6.7 2c-1.9-.2-3.7-1.6-4-2.6M254 224.1c2.7 2.2 3.9 4.2 3.9 7.5a8 8 0 0 1-4 7.5"/>
|
||||||
|
<path stroke-linecap="round" d="M251.5 236.4c4 5.1 6.3 8.1 6.4 14.1.1 5.7-1.7 9.6-5 13.7"/>
|
||||||
|
<path d="M329.8 169.3a4 4 0 0 0 1.5-2.2q.8-2.2-.2-4 1.3 2 .7 4c-.1 1-.8 1.5-1.6 2.3m51.5 86.1v16.2l-.1 2.5-.3 1.7"/>
|
||||||
|
<path d="M381.4 254v19.9l-.5 2.6m.5-43v14.6m.3-13.4v11.8m0-26.8v8.8m-.3-9.9v11m.3-19v3.5m-.3-4.2v5m-1.8 65.2-.4.7a19 19 0 0 1-4.1 5.7 20 20 0 0 1-5.9 4 25 25 0 0 1-6.5 2.2c-2.7.6-4.2.8-6.9.9-2.5 0-3.9 0-6.3-.2-2.7-.2-4.1-.5-6.8-.8-2.2-.2-3.4-.3-5.6-.3s-3.5 0-5.7.4a22 22 0 0 0-5.2 1.4c-2.7 1.1-5.7 3-6.4 3.8-.6-.8-3.7-2.7-6.3-3.8a22 22 0 0 0-5.2-1.4c-2.2-.4-3.5-.4-5.8-.4s-3.4.1-5.6.3c-2.6.3-4 .6-6.7.8-2.5.2-3.9.3-6.3.2a33 33 0 0 1-13.5-3 20 20 0 0 1-5.8-4.1l-2.5-2.8m-2-3.2a10 10 0 0 1-2.3 7.7c-.8.9-2.6 2.6-5 2.6-3.7 0-4.8-2.5-5-3.2"/>
|
||||||
|
<path d="M255.6 278.9q1 1 1.9 2.5c1 1.8.6 4.8-.1 6.2l-.3.4m-20.3 18q3.3 3.8 10.9 7.1c7.1 3 18.1 3.6 25.7 4 10 .3 19.3.7 25 7m17.3-4a12 12 0 0 1 4 5.5m-7.3 11.5-.7.7a8 8 0 0 1-6.6 2c-2-.2-3.8-1.6-4.3-2.6m-5.4-2.9.3.4a8 8 0 0 0 5.1 2.4m27 0a18 18 0 0 1-7.7 6.1 18 18 0 0 1-7.6-6.1l-.3-.5m15.6.4.7.7a8 8 0 0 0 6.7 2 6 6 0 0 0 4-2.5l.5-.7"/>
|
||||||
|
<path d="m339 336.6-.7 1.2-1.1 1-1.7.7h-1.6"/>
|
||||||
|
<path d="M343 325.3a8 8 0 0 1 2.4 2.9q.4 1 .5 2.3a6 6 0 0 1-1.5 4.2 8 8 0 0 1-5.4 2.4h-.4m.2-.2a7 7 0 0 1-5.2-2.2m63.7-67.9a24 24 0 0 1-4.8-6.4 19 19 0 0 1-1.7-4.5 12 12 0 0 1-.5-3.3 26 26 0 0 1 4.6-15.3c.7-.8 1.4-1.8 2.1-2.2m-1.3-75.9c2.5.2 4.8 3 4.8 5.7 0 3.8-1.3 5.5-4.4 9.3-2.6 3.2-10.6 9-10.3 14.5q.1 1.6 1.1 2.8m-3.2 3.5a7 7 0 0 0 2 1.4 5 5 0 0 0 4.3-.3M369 153a6 6 0 0 1 2.2 2.6c1.8 4.5-2.2 7.9-6 10.4a21 21 0 0 1-8.3 3.3"/>
|
||||||
|
<path d="M364.6 161.6a4 4 0 0 1-3.1-1.5l-.7-1m-15 4.9-1.2-1q-1.7-1.4-.8-4.4c.6-1.9 3.7-7.2 3.8-10.9.2-5.6-2-9-5.3-10.2"/>
|
||||||
|
<path stroke-linecap="round" d="m347.3 146.5-.1 2-.6 2.2-1 3-1 1.9-.8 1.9-.4 1.3-.2 1 .1.9m38 126.3.6.8c.7 1 3.2 3 5.5 3 3.7 0 4.6-2.6 4.7-3.2.5-2.9-.5-3.6-2-4.5 0 0-.8-.4-1.9-.2"/>
|
||||||
|
<path d="M237 274.4a7 7 0 0 1-3.7 0c-2.9-.9-5.2-3.6-4-7m13.4-31.8q.4.5.4 1c.4 3.8-2.8 4.8-5 4.2a6 6 0 0 1-3-2.3 5 5 0 0 1-.7-2.3m22-23.6q.9.7 1.3 1.7m-1.1-8.5q.8.7 1.1 1.3"/>
|
||||||
|
<path stroke-linecap="round" d="M257.9 210.5a9 9 0 0 1-1.6 2.4 12 12 0 0 1-8.8 4q-3 .1-6-1.7a10 10 0 0 1-4-5.6"/>
|
||||||
|
<path d="M255.4 195.3a8 8 0 0 1 2.4 3.4"/>
|
||||||
|
<path stroke-linecap="round" d="M257.8 203.2c-.9 3-3.5 6.6-8.6 7.9-2.4.6-5.6-.2-6.6-1"/>
|
||||||
|
<path d="M240 202.6c.3 2.6 2 4.6 5.4 4.6 4.7.1 7.6-6.7 3.4-11.5"/>
|
||||||
|
<path stroke-linecap="round" d="M229.4 225.5q1 1.3 2.4 2.4a17 17 0 0 0 6 3.3m5.2.5c4.2-.5 6.6-3.7 6-7.3-.3-2.8-2.8-5-4.6-5.1"/>
|
||||||
|
<path d="M249.8 188.1c1.9 0 3 1.6 2.9 3"/>
|
||||||
|
<path stroke-linecap="round" d="M249.4 163a12 12 0 0 0 5 5.9m144.2 31c1.7 2.3.6 7-4 7a5 5 0 0 1-4.5-2.5"/>
|
||||||
|
<path d="M381.7 169.1V185"/>
|
||||||
|
<path stroke-linecap="round" d="M243.8 202.3c1.4 1 3.3-.7 2.5-2.6-.5-1.2-2.2-2.6-4.7-.9-2.8 1.9-2 7.8 3.2 7.9 4.7 0 7.6-6.8 3.4-11.6-4-4.6-11.3-3.6-16 .2A21 21 0 0 0 225 207a23 23 0 0 0 0 9.2 21 21 0 0 0 3 7.5l1.3 1.7c.8.8 1 1.2 2 2a15 15 0 0 0 10.4 3.7c4.6-.2 7.3-3.4 6.8-7.3-.4-3.8-4.2-5.7-6.7-3.9-1.7 1.2-2.3 4.9.7 5.8 1.6.5 3.1-1.7 2-3M374 150.9q4-2.2 6.3 1a10 10 0 0 1 1.6 7.2 9 9 0 0 1-3.5 5.8"/>
|
||||||
|
<path stroke-linecap="round" d="M380.5 152c3.1-2 6.5-1.1 8.3 1.6 1.3 2 1.7 3.6 1.6 6.1a11 11 0 0 1-5.7 9.2"/>
|
||||||
|
<path d="M395 159.2c2.6.2 4.6 2.5 4.6 5.1 0 3.8-1 5.5-4 9.3-2.7 3.3-10.6 9-10.4 14.6 0 2.1 1.8 4 3.3 4.2"/>
|
||||||
|
<path stroke-linecap="round" d="M395.4 202.3c-1.5 1-3.3-.6-2.5-2.4.5-1.2 2.2-2.8 4.7-1.1 2.7 1.9 2 7.8-3.3 7.9-4.7 0-8-6.6-3.4-11.6 4-4.6 11.7-3.7 16.5.1 2 1.6 6.1 6 7 12 1 7 .9 15.6-6.4 21-3 2.1-7 3.1-10.6 3-4.6-.2-7.3-3.5-6.8-7.4.5-3.8 4-5.4 6.7-3.9s2.3 5.4-.7 5.8c-1.7.2-3.1-1.7-2-3"/>
|
||||||
|
<path d="M392.9 199.9c.8-3.5 3.7-3.8 6.2-3.8 6.5.1 11.1 8 11.2 15.5 0 9.5-4 15.2-11 15.5-1.9 0-5-.8-5-3"/>
|
||||||
|
<path stroke-linecap="square" d="M397 198.3c6.9 1.6 9.3 7.8 9.3 13.8 0 4.9-.5 11.6-10 13.9"/>
|
||||||
|
<path d="M408.4 265.3a3.9 3.9 0 1 0-6.3 2.4"/>
|
||||||
|
<path stroke-linecap="round" d="M394.4 259.4c1.4 2 3 4.1 6.3 6m-1.3 10.5c-3.2-2.2-9.5-5-15-2.2a8 8 0 0 0-4.4 4.4 10 10 0 0 0 1.8 9.5c.9 1 2.7 2.6 5 2.7 3.8 0 4.7-2.6 4.8-3.2.4-2.8-1.2-3.9-2-4.1-.7-.3-2.8-.2-3.2 1.3q-.3.9.2 2"/>
|
||||||
|
<path stroke-linecap="round" d="M340.5 328.4c1 2.2-.2 3.2-1.6 3.4-2.2.3-3.3-1.4-3.4-3a4.4 4.4 0 0 1 4.3-4.7c2.3 0 4.1 1.5 5 3.5q.5 1 .5 2.4a6 6 0 0 1-1.4 4.1 8 8 0 0 1-5.4 2.5c-4.2.1-7.5-3.8-7.5-7.8 0-7.7 11.4-12 16-13a84 84 0 0 1 17.9-2.4c3.5-.1 6.2 0 10.1-.5 3.5-.3 5.4-.5 9-1.3a27 27 0 0 0 12.6-6.4c2.9-2.7 4.5-4.5 5.9-8.2a17 17 0 0 0-1.3-13.9 14 14 0 0 0-10.3-6.8c-3.7-.5-7 1.1-9 4.8-1 1.8-.6 4.8.1 6.2a6 6 0 0 0 4.8 3c3.8 0 4.7-2.6 4.8-3.2.4-2.8-1.2-3.9-2-4.2-.7-.2-2.8-.1-3.2 1.4q-.3.9.2 2"/>
|
||||||
|
<path stroke-linecap="round" d="M337.2 316.2c-4.8 2.1-8.4 3.7-11.4 8.5a10 10 0 0 0-1.2 4.9c0 2.7 1.1 5.7 3.3 7.6a8 8 0 0 0 6.7 2c2-.2 3.7-1.6 4-2.6"/>
|
||||||
|
<path d="M385.1 224.1c-2.3.8-3.9 4.2-3.9 7.5a8 8 0 0 0 4 7.5"/>
|
||||||
|
<path stroke-linecap="round" d="M387.6 236.4c-4 5.1-6.3 8.1-6.4 14.1 0 5.7 1.7 9.6 5.1 13.7"/>
|
||||||
|
<path d="m365.9 152 .3-.5c1.7-2.4 4.7-3.1 6.9-1.5 2.6 2 3.3 5.4 2.6 9q-.9 3.3-4 5.5"/>
|
||||||
|
<path stroke-linecap="round" d="M265.1 150.8q-3.9-1.9-6.3 1a9 9 0 0 0-1.6 7.2c.6 2.7 1.4 3.8 3.5 5.8"/>
|
||||||
|
<path d="M258.6 152a6 6 0 0 0-8.3 1.6 9 9 0 0 0-1.6 6.1c.2 4.2 2.8 7.6 5.8 9.2"/>
|
||||||
|
<path d="M249.7 154.8a7 7 0 0 0-6 6.6c0 4.5 1 6.3 3.5 9.6 2.5 3.4 10.7 9.6 10.7 16.7q.2 6.4-4.3 8.4c-2 1-4.3 0-5.4-1-2.6-2.4-1.5-6.5 1.2-7 3.3-.6 3.9 4.6.7 4.3"/>
|
||||||
|
<path d="M244 159.2c-2.5.2-5 2.3-5 5 0 3.8 1.5 5.6 4.6 9.4 2.6 3.3 10.1 9 9.9 14.5 0 2-1.5 4.6-2.9 4.3"/>
|
||||||
|
<path stroke-linecap="round" d="M238 236.1c1.3-2.9 4.4-1.6 4.6 0 .4 3.7-2.8 4.8-5.1 4.2a4 4 0 0 1-2.5-2 5 5 0 0 1-.4-3.7l.9-1.6 1.2-1.2q1.3-.7 3.4-.6c5.5 0 10.4 6.5 12 13.4.6 2.2 1.3 7.3.3 12a22 22 0 0 1-5.8 11.3 26 26 0 0 1-10 5.8 7 7 0 0 1-3.9.1c-2.8-.9-4.6-3.5-3.2-7 1.2-2.6 5.4-4 7.3-.6q.3.5.4 1.5c0 .9-.4 2-1 2.4-1.4.9-3.7.6-3.6-2"/>
|
||||||
|
<path d="M233.8 270.4c1 .4 1.6.3 2.9-.2l1.8-1c2.6-1.5 5.6-3.8 8.4-9.1a19 19 0 0 0 1.7-4.5q.4-1.6.6-3.3a26 26 0 0 0-4.8-15.3c-1.1-1.6-2-2.5-4.2-2.6m-9.5 31a3.9 3.9 0 1 1 6.3 2.3"/>
|
||||||
|
<path d="M232.2 261.4a4 4 0 0 1 3.7-3 3.7 3.7 0 0 1 3.6 3.7 4 4 0 0 1-1 2.6"/>
|
||||||
|
<path d="M239.4 261.3a16 16 0 0 0 6.2-12.4c0-4.1-1.6-8.4-3.6-10"/>
|
||||||
|
<path stroke-linecap="round" d="M244.7 259.4a17 17 0 0 1-6.3 6"/>
|
||||||
|
<path d="M254.6 273.7q-1.4-3.2-5.8-3.5-4.3-.3-8.2 1.9a19 19 0 0 0-10.8 17 25 25 0 0 0 2 9.5c.9 1.6 3 9 15.3 14a86 86 0 0 0 25.7 3.9c10.4.4 20 .8 25.6 7.6"/>
|
||||||
|
<path stroke-linecap="round" d="M239.7 275.9c3.3-2.2 9.5-5 15.1-2.2a8 8 0 0 1 4.3 4.4 10 10 0 0 1-1.8 9.5c-.9 1-2.7 2.6-5 2.7-3.8 0-4.7-2.6-4.8-3.2-.4-2.8 1.2-3.9 2-4.2.7-.2 2.8-.1 3.2 1.4q.4.9-.2 2"/>
|
||||||
|
<path d="M252.7 285.7q.6-1.6-.8-3.3a5 5 0 0 0-6-1q-1.1.6-2.4 2.2-.7.7-1.2 1.6-1 1.7-1 2.5c-2.5 7 1.5 14.4 6.5 17.6 4.4 2.8 8.8 3.6 14.4 4 2.5.3 4 .3 6.5.5h16.8c3 .3 5.1.4 7.6.8q2.5.3 5.4 1 .9 0 1.8.4l1.2.3q5.5 1.6 9.8 4.2 1.3.7 2.5 1.7l1.3 1.2c2 2 4 4 4.4 6.7v1.6c0 1.8-1.4 4.3-5.3 5"/>
|
||||||
|
<path d="M298.6 328.4c-1 2.2.2 3.2 1.6 3.4 2.2.3 3.3-1.4 3.5-3a4.4 4.4 0 0 0-4.4-4.7 6 6 0 0 0-5 3.5 7 7 0 0 0-.5 2.4 6 6 0 0 0 1.4 4.1 8 8 0 0 0 5.4 2.5c4.2.1 7.5-3.8 7.5-7.8 0-7.7-11.4-12-16-13a84 84 0 0 0-17.9-2.4c-3.5-.1-6.2 0-10.1-.5-3.5-.3-5.4-.5-9-1.3a27 27 0 0 1-12.5-6.4 17 17 0 0 1-4.7-22 14 14 0 0 1 10.3-6.9q5.9-.7 9 4.8c1 1.8.6 4.8-.1 6.2a6 6 0 0 1-4.8 3c-3.8 0-4.7-2.6-4.8-3.2-.4-2.8 1.2-3.9 2-4.2.7-.2 2.8-.1 3.2 1.4q.3.9-.2 2"/>
|
||||||
|
<path stroke-linecap="round" d="m273.3 152-.4-.5c-1.7-2.4-4.7-3.1-6.9-1.5-2.6 2-3.3 5.4-2.5 9a9 9 0 0 0 4 5.5"/>
|
||||||
|
<path d="M366.8 159.6c-4 4.4-8.1 5.8-14.1 6-2 0-5.5-.6-7.6-2.1-1.3-1-2.8-2.6-1.9-5.5.6-1.9 3.7-7.2 3.8-10.9.3-5.6-1.9-8.7-5.3-9.9-6.2-2.2-13 4-17 5.4-2.1.7-3.2.8-5.1.8-2 0-3-.1-5.2-.8-4-1.4-10.7-7.6-17-5.4-3.4 1.2-5.5 4.3-5.3 10 .1 3.6 3.2 9 3.8 10.8 1 2.9-.5 4.5-1.9 5.5-2 1.5-5.7 2.1-7.5 2-6-.1-10.1-1.5-14.1-5.9"/>
|
||||||
|
<path stroke-linecap="round" d="M297.3 314.4c.8.3.2-.2 5.3 2a22 22 0 0 1 11.3 8.9 11 11 0 0 1 .9 7.3"/>
|
||||||
|
<path d="M297.7 336a8 8 0 0 0 3.2.9c4.2.1 7.5-3.8 7.5-7.8 0-2.8-1.5-5.2-3.6-7"/>
|
||||||
|
<path stroke-linecap="round" d="M298.6 328.4c-1 2.3.4 3.5 1.8 3.7 2.2.2 3.4-1.4 3.6-3a5 5 0 0 0-2.2-4.2"/>
|
||||||
|
<path d="M390.1 154.8c3.2 0 6 3.6 6 7.2 0 4.3-2.2 6.9-3.9 8.8q-1.9 2.3-4.4 4.7"/>
|
||||||
|
<path stroke-linecap="round" d="M386.3 151.4a9 9 0 0 1 2.8 2.4c1.3 2 1.7 3.7 1.6 6.2-.2 4.2-3.2 7.1-6 9m-4.7-17.6.6.7c1.9 2.2 2 5.4 1.6 7.2a8 8 0 0 1-3.8 5.4m-5-14.4c2.6 2 3.4 5.4 2.5 9q-1 3.6-4.2 5.2m11.1 41.1c.3 1 .9 1.3 1.5 2a14 14 0 0 0 6.2 3.5q3.7.9 6.3-.9m-163 54q2 .1 3.3 2.3.3.4.4 1.5 0 1.5-1 2.2c-1.5 1-4 .5-4-2"/>
|
||||||
|
<path d="M241.5 231.3c5 1 9.7 6.9 11.2 13.3.6 2.3 1.3 7.3.3 12a22 22 0 0 1-6 11.4l-2.1 1.9-1 .7m-8-12.1c2 0 3.8 1.9 3.8 4a4 4 0 0 1-1 2.6"/>
|
||||||
|
<path d="M234.6 260.7c2.1 0 4.1 2 4.1 4.2a4 4 0 0 1-1.4 3"/>
|
||||||
|
<path stroke-linecap="round" d="M254 239.5a18 18 0 0 1 3.8 7.7m0 8.5a17 17 0 0 1-1.5 4 18 18 0 0 1-3.6 4.7"/>
|
||||||
|
<path d="M254.3 224.3q2.7 2.2 3.5 4.8"/>
|
||||||
|
<path stroke-linecap="round" d="M257.9 219.5a10 10 0 0 1-3.4 4.6m-9.2-17.2 2.2-.6 1.3-1 .8-1.1.7-1.8.3-1.5"/>
|
||||||
|
<path d="m241 199.3-2.5.8a9 9 0 0 0-3.5 3 17 17 0 0 0-2.2 12.7 16 16 0 0 0 2.3 5.6l1 1.4c1.4 1.3 2.6 2 4.6 1.7"/>
|
||||||
|
<path stroke-linecap="round" d="M253 189.8c-.3 1.3-1 2.9-3 2.7"/>
|
||||||
|
<path d="M245.7 198.5c-2-1.9-6-2.4-10.1.2L234 200l-1.4 1.6a18 18 0 0 0-2.4 5c-.7 3-.7 5.6-.6 6.3q0 1.5.3 2.7 1 4.2 2.3 6.2c.9 1.5 3 5 7.7 5.4 1.8.1 4.8-.7 5-3"/>
|
||||||
|
<path stroke-linecap="round" d="M363.8 157c.3-1.6 2.3-1.9 3-1 1.2 1.6.4 4.2-2 4.9a4 4 0 0 1-3.8-1.4c-1-1.3-.9-2.8-.5-4q.4-1.2 1.7-2.2c2.7-2 7.1-1.6 8.6 2 1.8 4.4-2.2 7.8-6 10.3-4.6 3.2-10 3.8-14 3.7-9.2 0-16.1-4.4-20.7-7q-1.7-.7-2.7.3a2 2 0 0 0 .3 2.7"/>
|
||||||
|
<path stroke-linecap="round" d="M365.6 155.5c1 0 1.2.4 1.5.8 1.2 1.5.3 4.1-2 4.9m17.8 51.5c-3.5 3.8-.2 10.3 2.4 11.8.9.7 1.3.3 2 .7"/>
|
||||||
|
<path d="M383.1 205.4q-1.5 1-1.6 3.3a5 5 0 0 0 1.4 4 14 14 0 0 0 9.3 3.7q3 .1 6-1.7a9 9 0 0 0 3.8-5.4m-20.8 61.8-.2 2.5a19 19 0 0 1-2 7 19 19 0 0 1-4.2 5.6 20 20 0 0 1-5.9 4 25 25 0 0 1-6.5 2.3 44 44 0 0 1-13.2.6c-2.7-.2-4.1-.5-6.8-.8-2.2-.1-3.4-.3-5.6-.3a28 28 0 0 0-10.9 1.9c-2.7 1-5.7 3-6.4 3.8-.6-.9-3.7-2.8-6.3-3.8a22 22 0 0 0-5.2-1.5c-2.2-.4-3.5-.4-5.8-.4s-3.4.2-5.6.4c-2.6.2-4 .6-6.7.7-2.5.2-3.9.3-6.3.2a33 33 0 0 1-7-.8 25 25 0 0 1-6.5-2.2 20 20 0 0 1-5.8-4.1 19 19 0 0 1-4.2-5.7 19 19 0 0 1-2-6.9c-.2-1-.2-2.5-.2-2.5V169.3h123.2z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#c7b37f" stroke="#c7b37f">
|
||||||
|
<path stroke-width=".3" d="M248 285.6a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0zM232.5 268q.2-2.1 1.8-2.3c1.6-.2 1.7 1 1.7 2.3q-.2 2-1.7 2.2-1.6-.2-1.8-2.2z"/>
|
||||||
|
<path stroke="none" d="M241.3 223.6q.2-1.7 1.7-1.8 1.6.2 1.7 1.8c.1 1.6-.7 1.8-1.7 1.8s-1.7-.8-1.7-1.8M272 158c0-1 .5-2 1.4-2q1.5 0 1.8 1.6c0 1-.5 2-1.4 2q-1.4 0-1.8-1.6"/>
|
||||||
|
</g>
|
||||||
|
<g stroke="#c7b37f" stroke-linecap="round" stroke-width=".6">
|
||||||
|
<path d="M239.3 234q-.6.1-.8.5-.4.3-.6.9l-.2 1.2m4.7 26.7 1-1 .6-1 .5-1 .7-1.3m-1.3 14-1.5.7-1.1.6-1.3.8-1.2 1m15-37.9-.8-.8-1-.8-.9-.8"/>
|
||||||
|
<path stroke-linecap="butt" d="m254.2 225-1.2.5-1.5.3"/>
|
||||||
|
<path d="m237.4 208.4.5 1.5q.3 1 .9 1.7a8 8 0 0 0 2.6 2.7l1.5.8m-1-5.8 1.3.6a7 7 0 0 0 3 .6l1.8-.1m7.2-40.7-2-1.2q-1.2-.7-2-1.5l-1.1-1.3-.8-1.3m7.5-4.6.6 1.7 1.4 2c1 1 1.7 1.3 2.8 2.2m1.4-6 .7 1.6.8 1.2 1.3 1.3q1 .7 2 1.1"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#703d29" stroke-width=".2" d="M333.3 151.6c0-1.7-1.7-1.8-2.4-1.8-1.8 0-2.3 1.1-4.6 2.3a12 12 0 0 1-6.7 2 12 12 0 0 1-6.7-2c-2.3-1.2-2.7-2.3-4.6-2.3a2.3 2.3 0 0 0-2.2 2.4v.9l.3.2q0-1 .5-1.7a2 2 0 0 1 1.6-.8c1.8 0 2.5 1.2 4.8 2.4 3 1.6 4.2 1.9 6.7 2a12 12 0 0 0 6.8-2c2.3-1.2 3-2.5 4.8-2.5q.9 0 1.3 1v.9l.2.1z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#703d29">
|
||||||
|
<path d="M264.4 294c.5-.5.9-.3 1-.6q0-.2-.3-.3l-.9-.2-.8-.4h-.5c-.1.4 1 .4.6 1.4l-.8 1.2-2.6 3-.2.1v-4.3l.1-1.8c.2-.4.8 0 .9-.4q0-.2-.3-.3t-1.1-.3l-1-.5h-.6l.1.3q.5.2.5 1v7.4q0 .7.2.7l.4-.3z"/>
|
||||||
|
<path d="M267.5 295.2c.3-1.1 1-.4 1-.8q.1-.2-.2-.3l-1.3-.4-1.2-.4h-.4c-.1.5 1.1.5.8 1.5l-1.7 5.5c-.3 1-1 .6-1.1 1v.1l1.2.4 1.6.5h.3c.2-.4-1.2-.3-.7-1.7zm3.7 1q.3-.7.9-.4 1.6.6 1 2.5c-.2.6-.4 1.2-2 .8q-.6-.1-.6-.5l.7-2.3zm-2.8 5c-.5 1.4-1.2.8-1.3 1.2q0 .2.3.3l1.6.4.8.3h.4c.1-.5-1-.3-.7-1.5l.6-2q0-.6.6-.3.8.1.8.8l.3 2q.1 1.4 1 2 1 .1 1.4-.4l-.2-.2h-.3s-.3 0-.3-.3l-.7-3.6q.1-.2.8-.3a2 2 0 0 0 1-1.3c.1-.5.4-2.2-1.8-2.9l-2.1-.5-1.2-.4h-.3c-.1.5 1.1.4.7 1.7zm8.4 2.5c-.4 1.4-1.4.5-1.5 1q0 .3.3.3l1.5.3 1.4.4q.4.2.6-.1c0-.3-1.3-.3-1-1.8l1.3-5.2q0-.8.6-.5l1 .2c1.1.3.5 1.5 1 1.6q.2-.1.2-.6l.1-1v-.4l-3.3-.7-3.2-.8q-.2 0-.2.2l-.5 1.5q-.2.3 0 .4c.5.1.5-1.5 1.7-1.2l.9.2q.6 0 .4.8zm12.7-3.3c.4-.6.8-.5.9-.7q0-.2-.4-.3h-.9l-.9-.3q-.3-.1-.4.1c-.1.4 1 .2.8 1.3q0 .3-.6 1.3l-2 3.3-.3.2v-.2l-.7-4-.1-1.8c0-.5.7-.2.7-.5q.1-.2-.4-.3l-1.1-.1q-.6 0-1-.3-.4-.1-.6.1l.1.2q.7.2.7.9l1.3 7.3.3.7.4-.3zm.6 6.8q0 .3.2.5l1.7.7c1.4.2 2.6-.7 2.8-2.2.3-1.5-.3-2.1-1.4-2.9-1.3-.9-1.8-1.1-1.7-2q.3-1 1.4-1c1.8.3 1.6 2.6 1.8 2.6q.4 0 .3-.4l.2-1.6v-.4h-.6c-.4 0-.7-.5-1.6-.7q-2-.1-2.5 2-.1 1.6 1.2 2.4c1.6 1.1 2.2 1.4 2 2.4q-.3 1.5-1.7 1.3c-1.2-.2-1.6-1.4-1.8-2.6q0-.3-.2-.3-.2.1-.2.5v1.7zm15.8-4.5c.3-.7.8-.6.8-.9q0-.2-.4-.2h-.9l-.9-.1q-.3 0-.4.2c0 .4 1 0 1 1.1q0 .3-.5 1.4l-1.8 3.5-.1.3-.1-.3-1.1-4-.3-1.6c0-.5.7-.3.7-.6q.1-.2-.4-.2h-1.2l-1-.2q-.4-.1-.6.1l.2.2q.6.2.7.8l2.1 7.1.4.7q.2.1.3-.4z"/>
|
||||||
|
<path d="M307.6 308.5c0 1.2-1 1-1 1.5q0 .2.3.1h2.2l.4-.1c0-.6-1.4.2-1.4-2v-4.2l.1-.1.2.1 5.1 6.3.3.1.2-.3v-6.7c0-1.3 1-1 1-1.3l-.3-.2h-2.3q-.2 0-.2.2c0 .4 1.3.2 1.3 1.3v4l-.1.4-.4-.3-4.2-5.3q-.1-.4-.4-.3h-1.8l-.2.1c0 .6 1.2-.2 1.2 2.1zM318 303c0-1.1.8-.7.8-1.1q.1-.2-.4-.2h-2.6s-.3 0-.3.2c0 .4 1.1 0 1.1 1.2v5.7c0 1.1-.8.8-.8 1.2l.2.2h2.8q.3 0 .3-.2c0-.4-1.2.2-1.2-1.3zm4.5 5.5c0 1.5-1.2 1-1.2 1.4q0 .3.4.2h3q.5 0 .5-.3c0-.3-1.4 0-1.4-1.4V303q-.1-.7.5-.6h1c1.2-.1.8 1.2 1.3 1.2q.2-.1.1-.6l-.1-1q0-.3-.2-.4l-3.3.1h-3.3l-.2.3-.1 1.6.1.4c.5 0 .2-1.6 1.4-1.6h.9q.5-.1.6.6v5.6zm6.3-2.2h-.4l.1-.5.7-2.2v-.2l.2.1 1 2.1.2.4q0 .3-.4.2zm1.8.5c.3 0 .3 0 .8 1l.2.8c0 .7-.7.6-.7 1q0 .2.4 0h1.2l1.3-.1q.4 0 .4-.2c0-.4-.6 0-1-.7l-3.4-7-.3-.4q-.2 0-.3.4L327 309c-.2.7-.8.7-.7 1h2.3q.4 0 .5-.3c.1-.3-1.2 0-1.3-.9l.2-1q.3-1 .6-.8l2.1-.2zm8.3-5c-.1-.8 0-.8 1.2-1 2-.2 1.4 1.3 2 1.2q.2-.1 0-.6l-.1-1.1q0-.2-.3-.2-1.4 0-2.4.3l-2.8.4q-.3 0-.3.2c.1.5 1.3 0 1.4 1l.7 5.5c.2 1.5-.7 1-.6 1.5h.2l1.4-.1 1.2-.1q.5 0 .5-.3c0-.3-1.2.1-1.4-1.2l-.2-1.7q-.2-1 .3-1h.8c1.1-.2 1 1.1 1.3 1q.4-.2.1-.5l-.3-2.1q-.1-.4-.2-.3c-.3 0-.1 1.1-1 1.2l-.7.1q-.6.2-.6-.5zm4 2.8c.4 2.3 2.1 3.7 4.2 3.3 3.4-.7 3.5-3.6 3.2-5.3-.5-2.5-2.3-3.7-4.4-3.3-2.5.5-3.5 2.7-3 5.3m1.1-1c-.3-1.6 0-3.4 1.7-3.7 1.4-.3 3 .8 3.4 3.4.3 2 0 3.6-1.8 4s-3-2-3.3-3.6zm8.3-4.1q0-.9.6-.9 1.6-.2 2.1 1.6c.2.7.3 1.4-1.3 1.8q-.5.1-.8-.2l-.5-2.3zm0 5.7c.4 1.4-.5 1.3-.5 1.6q.2.3.4.1l1.6-.4 1-.2q.3 0 .2-.2c0-.4-1 .3-1.3-1l-.5-2c0-.4-.2-.4.4-.5q.6-.3 1.1.3l1.3 1.6c.5.6 1 1.3 1.8 1.1q.9-.2 1-.9l-.2-.1-.3.1s-.3.1-.4 0l-2.4-2.9.5-.6q.4-.6.2-1.6c-.1-.5-.7-2.1-3-1.6l-2.1.6-1.2.2q-.3 0-.2.2c0 .5 1.1-.2 1.4 1zm8.7-2c.3 1.4-1 1.2-.9 1.6q.1.3.5.2l1.4-.5 1.5-.3q.5.1.4-.4c0-.3-1.3.4-1.7-1l-1.3-5.3q-.2-.7.3-.7l1-.2c1.1-.4 1.1 1 1.5.9s0-.5 0-.7l-.4-1s0-.3-.2-.2l-3.2.9-3.2.7v.3l.1 1.6q0 .4.3.4c.5-.1-.3-1.6 1-1.9l.8-.2q.6-.2.7.5zm5.5-7.3c-.3-1 .6-.9.4-1.3h-.3l-1.4.4-1.2.3s-.3 0-.3.2c.1.4 1.2-.2 1.5.8l1.6 5.6c.2 1-.6 1-.5 1.3q0 .2.2.1l1.1-.3 1.6-.4q.4 0 .3-.3c-.1-.3-1.1.5-1.5-.9zm2.3 2.7c.7 2.3 2.6 3.4 4.7 2.7 3.2-1.1 3-4.1 2.4-5.7-.8-2.4-2.8-3.3-4.8-2.7-2.4.9-3.2 3.2-2.3 5.7m1-1c-.6-1.7-.6-3.5 1.1-4 1.3-.5 3 .4 3.9 2.9.6 1.8.5 3.6-1.2 4.2-1.8.6-3.2-1.5-3.8-3.2zm7.6-5.5q-.2-.9.4-1 1.7-.3 2.4 1.4c.2.6.4 1.3-1.1 1.9q-.6.2-.8 0zm.8 5.6c.6 1.4-.4 1.4-.2 1.7q0 .3.4.1l1.5-.7.9-.2q.3-.1.2-.3c-.2-.4-1 .4-1.4-.8l-.8-1.9q-.4-.5.3-.7.6-.3 1.1.3l1.6 1.4c.5.5 1.1 1.1 2 .8.3-.2.9-.7.7-1l-.2-.1-.2.2h-.5l-2.8-2.5.4-.7a2 2 0 0 0 0-1.6c-.1-.6-1-2-3.1-1.2l-2 .9-1.2.4-.2.2c.2.4 1.1-.4 1.6.8l2 5z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#fedf00" transform="matrix(.64 0 0 .64 0 16)">
|
||||||
|
<path fill="#d52b1e" d="M412.7 249.3h82.1v82h-82.1z"/>
|
||||||
|
<path id="ad-a" fill="#fff" d="M451.2 313.8s0 3-.8 5.3c-1 2.7-1 2.7-1.9 4a13 13 0 0 1-3.8 4q-3 1.9-6 1.6c-5.4-.4-8-6.4-9.2-11.2-1.3-5.1-5-8-7.5-6q-2 1.7-.3 4.6a9 9 0 0 0 4.1 2.8l-2.9 3.7s-6.3-.8-7.5-7.4c-.5-2.5.7-7.1 4.9-8.5 5.3-1.8 8.6 2 10.3 5.2 2.2 4.4 3.2 12.4 9.4 11.2 3.4-.7 5-5.6 5-7.9l2.4-2.6 3.7 1.2z"/>
|
||||||
|
<use xlink:href="#ad-a" width="100%" height="100%" transform="matrix(-1 0 0 1 907.5 0)"/>
|
||||||
|
<path d="m461.1 279 10.8-11.7s1.6-1.3 1.6-3.4l-2.2.4-.5-1.2-.1-1.1 3-.7V260l.3-1.3-3.2.2.3-1.4.5-1 1.9-.4h1.9c1.8-3.4 9.2-6.4 14.4-1 3.8 4 3 11.2-2 13.2a6 6 0 0 1-6.8-1.1l2-4c2.7 1.7 5-.3 4.8-2.4-.2-2.7-2-4.3-4.3-4.5q-3.5-.1-5 3c-.6 1.3-.3 2.2-.5 3.6-.2 1.5 0 2.3-.5 3.8a9 9 0 0 1-2.4 3.6l-11 12-43 46.4-3.2-3z"/>
|
||||||
|
<path fill="#fff" d="M429.5 283s2.7 13.4 11.9 33.5c4.7-1.7 7.4-2.8 12.4-2.8s7.6 1 12.3 2.8A171 171 0 0 0 478 283l-24.2-31z"/>
|
||||||
|
<path d="m456.1 262.4 16.8 21.7s-2.2 10.5-9 26.3c-2.7-.6-5-1.1-7.8-1.3zm-4.7 0-16.8 21.7s2.2 10.5 9 26.3c2.7-.6 5-1.1 7.8-1.3z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#d52b1e">
|
||||||
|
<path fill="#fedf00" d="M322.3 175.5h52.6V228h-52.6z"/>
|
||||||
|
<path d="M329.7 175.5h7.8V228h-7.8zm15 0h7.8V228h-7.8zm15 0h7.9V228h-7.9z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#d52b1e" stroke="#d52b1e" stroke-width=".5">
|
||||||
|
<path fill="#fedf00" stroke="none" d="M264.3 273.5q.1 1.6 1.4 4.3c1 1.5.6 1.4 2.7 3.8a15 15 0 0 0 4 2.9 33 33 0 0 0 15 2.6q4-.2 6.6-.7a71 71 0 0 1 11-.6q2.2 0 4.7.6c3.5.7 7 2 7 2v-54.7h-52.6V271l.2 2.4z"/>
|
||||||
|
<path stroke-width=".3" d="m270.4 283.1 2.5 1.5 3.4 1.2v-52.2h-5.9zm29.2 2.4v-51.9h-5.8v52.8l5.8-.7zm11.7-51.9h-5.8v52.1l5.8 1zm-23.4 0V287s-3.8.2-5.8 0v-53.4z"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(.64 0 0 .64 0 16)">
|
||||||
|
<path fill="#fedf00" d="M585.5 402.4a21 21 0 0 1-2.2 6.6c-1.5 2.3-1 2.3-4.3 6a26 26 0 0 1-13 7 52 52 0 0 1-16.6 1.6q-6.3-.4-10.3-1c-3.8-.6-6.7-.9-11-1h-6.2a83 83 0 0 0-18.3 4.2V340h82.2v58.5z"/>
|
||||||
|
<g id="ad-b">
|
||||||
|
<path fill="#d52b1e" d="m524.6 347-.6.2-.8.8q-.5.5-1.2.8l-.6.5c-.3.3 0 .6-.3 1q-.1.5-.6 1t-1 1l-1.2 1-.3.1h-.6l-.8.8.3.6.8 1.4q.2.6.5.8.7.3 1.3.1l2 .5 1.5.8q.6.4 1.3.5h1.8v.3l2 1-.1.4q-.2.5-.1.8 1 2.9 1.5 3.2.8.4 1.1 1.5l-.3.3q-1 .8-1.7 1.8c-.7 1.2-1.2 1.2-.3 2.8l1.5 2.4.8 2q.3 1 .3 2l1 .3.7-.6.6-1.2v-1q-.3-.2-.2-.7c0-.4.5-.3.7-.6.3-.5-.4-.8-.7-1.1-.6-.7-1.4-.9-1.6-1.9q-.1-.3.4-.7l2-1.8q.4.2 1 .1l1.3.4h1.6l.1.6c.1 1-.1 3 .2 3.5l.3.6.2.6v2l-.2 1.7q0 .6-.5 1t-1 .7v1l1.1.5 1.3.3.7-.3.1-.6.5-.5.9-.1q.2-.3 0-.8 0-.8-.3-1.6l-.1-2.8q0-.8.2-1.5c.1-1 .4-1.4.6-2.2l.4-2.5a24 24 0 0 0 10.1-.6q1.2 1 2.7 1.6v1q0 .4.2.7l.3.3q.4 0 .7-.2t.2-.7v-.7h1.8v1.1q.2.4.5.4h.6q.3-.4.3-1v-.7l1-.4v.9l-.3.9c-.2.6-.5.8-.8 1.4q-.4.8-1 1.5l-.6.7-.6.9-.9 1c-.7.6-1.2.2-2 .9l-.3 1 1.4.6 1.3.2.4-.2q0-.5.3-.8t.7-.4q.6 0 1-.2.4-.6.7-1.5a13 13 0 0 1 3-3.9l1.7-1.4q.4-.4.5-1l-.2-.6-.2-1c1.5.7 1 .7 1.2 1.4.3.6 0 1 .1 1.7.1.8.5 1.1.5 1.9q0 1.1-.3 2.3 0 1-.5 2a4 4 0 0 1-1.1 1.5l-.6.5-.1 1 1.1.4 1.6.4.4-.3c.2-.7 0-1.7.4-1.7q.6 0 .8-.3v-.7l.7-4.5.4-1.9.4-1.7c.7-2-.2-2.3-1-3.6q-.7-.9-.7-1.5v-5.7l.4-.2c1.2-.7 1.7-.9 2.4-2.5l.3-1.5v-1l-.4-1-.6-.8c-.7-1-1.7-1.1-2.7-1.5-1.5-.5-2.5-.4-4-.5-1.8-.2-2.7-.2-4.4 0-2 0-3.1.4-5.1.7l-4.9.4c-2.3 0-4.4-.5-5.8-.4-2.4.2-2.5.8-6.2 1.1l-3.8.2-2.2-.7c.9-.3 1.1-.5 1.5-1s.2-.7.6-1.1l.7-1-.9-.4h-1l-1.2.3-.8.6-2.2-1.2a9 9 0 0 0-3-.9zm2 11.8"/>
|
||||||
|
<g fill="none" stroke="#fedf00" stroke-linecap="round">
|
||||||
|
<path d="m568.8 359.5-.8.3q-1.2.5-2.6.5c-2.6.2-4.3-1.1-7-.9-1.4.1-2 1.2-3.5 1.6l-1.7.2.5-1s-1.2.3-2 .3l-1.6-.2 1-1-1.3-.2-1-.7 1.7-.3c1.5-.4 2-1.2 3.9-1.4 1.1 0 3 0 7.6.8 3 .5 4.4.2 5.5-.3q1-.5 1.1-1.8 0-1.2-.8-1.8l-1.1-.4"/>
|
||||||
|
<path fill="#fcd900" stroke-linecap="butt" stroke-width=".5" d="M524.8 350.6q-.7 0-1.3.3-.6.5-1 1.1.7.3 1.2.3t.8-.5q.4-.5.4-1.2z"/>
|
||||||
|
<path d="m536 363.8 1 2.3c.2.8 0 1.2.2 2v1.6m6.8-7-.3 1.3-1 3.5v.7m-11-4c.9.2.6 3.3 1.9 4"/>
|
||||||
|
<path stroke-linecap="butt" d="m560.1 369.8.4-.3a8 8 0 0 0 2.7-1.8"/>
|
||||||
|
<path d="M552.4 368c3.5-.9 5.9-2.6 7.6-2.9m-4-1.5h.8c1.5-.3 1.7.6 2.7 1.2 1.9 1 2.1 2.3 4.3 3.4l.4.1.8.4"/>
|
||||||
|
<path fill="#fcd900" stroke-linecap="butt" stroke-width=".5" d="M517.7 354.5h.7l.8-.2q.4 0 .7.2.2 0 .3.3t.1.5q0 .3-.6.4-.3 0-.5-.3v-.4a1 1 0 0 1-.9 0z"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#0065bd" d="m525.1 364.2-2-.9 1-.5.5-1.3q.1-1 .7-1.4t1.1-.1q.7.1.9.7 0 .8-.3 1.5l-.2 1.4q0 .6.4 1l-2-.4zm-1 1a.6.6 0 1 1 .7.5.6.6 0 0 1-.7-.6zm-1.7-16.6h-.2l-.6-1.2-.3-1.2v-2q0-.5-.2-.9c0-.2-.4-.3-.3-.4h.4q.5 0 1 .4t.6 1l.4 1.5.3.8.5.6-.7.8zm3.6 10.6 2.2 1a9 9 0 0 0 3.5-3.8c.9-1.8 1-2.7 1.4-4.4l-1.8-.5h-.4c-.5 1.8-.7 2.7-1.6 4.2q-1.2 2-2.6 3zm5 18.2.8-1.3 1.4-1.1h.4a9 9 0 0 1-.5 2.8l-.4 1-.5.5c-.5-.8-1.3-1.3-1.3-2zm33 1.8 1.4.6 1.5.9v.5l-1.5.2h-2.3l-.6-.4c.5-.7.8-1.6 1.4-1.8zm-9.8-2 1.4.5 1.5 1v.4a9 9 0 0 1-2.7.3l-1-.1-.7-.3c.6-.7.9-1.7 1.5-1.8m-17.4 2.1 1.5.5 1.5 1v.5a9 9 0 0 1-2.8.2h-1l-.6-.4c.5-.7.8-1.6 1.4-1.8m-9-29.8q-1-.6-.6-1.6l.6-.4q.2-.4 0-.8l-.1-1-.2-1q-.1-.8.4-1.6.5-.5.8-.6.2.3 0 .8 0 .6.3 1.2l.7 1.3.4 1.4-.2 1.2-.6.8-.8.4z"/>
|
||||||
|
</g>
|
||||||
|
<use xlink:href="#ad-b" width="100%" height="100%" y="36.6"/>
|
||||||
|
</g>
|
||||||
|
<path fill="none" stroke="#703d29" stroke-width=".5" d="M264.1 175.5h52.6V228h-52.6zm58.2 0h52.6V228h-52.6zm-58 98q.1 1.6 1.4 4.3c1 1.5.6 1.4 2.7 3.8a15 15 0 0 0 4 2.9 33 33 0 0 0 15 2.6q4-.2 6.6-.7a71 71 0 0 1 11-.6q2.2 0 4.7.6c3.5.7 7 2 7 2v-54.7h-52.6V271l.2 2.4zm110.4 0a13 13 0 0 1-1.4 4.3c-1 1.5-.6 1.4-2.7 3.8a15 15 0 0 1-4 2.9c-1.3.7-2.3 1-4.4 1.6a33 33 0 0 1-10.6 1q-4-.3-6.5-.7l-7.2-.6H334q-2.2 0-4.7.6c-3.5.7-7 2-7 2v-54.8H375v37.5l-.2 2.4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ae" viewBox="0 0 640 480">
|
||||||
|
<path fill="#00732f" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="#fff" d="M0 160h640v160H0z"/>
|
||||||
|
<path fill="#000001" d="M0 320h640v160H0z"/>
|
||||||
|
<path fill="red" d="M0 0h220v480H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 266 B |
@@ -0,0 +1,81 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-af" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd" stroke-width="1pt">
|
||||||
|
<path fill="#000001" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#090" d="M426.7 0H640v480H426.7z"/>
|
||||||
|
<path fill="#bf0000" d="M213.3 0h213.4v480H213.3z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#fff" fill-rule="evenodd" stroke="#bd6b00" stroke-width=".5" transform="translate(1 27.3)scale(1.06346)">
|
||||||
|
<path d="M319.5 225.8h8.3c0 3.2 2 6.6 4.5 8.5h-16c2.5-2.2 3.2-5 3.2-8.5z"/>
|
||||||
|
<path stroke="none" d="m266.7 178.5 4.6 5 57 .2 4.6-5-14.6-.3-7-5h-23l-6.6 5.1z"/>
|
||||||
|
<path d="M290 172.7h19.7c2.6-1.4 3.5-5.9 3.5-8.4 0-7.4-5.3-11-10.5-11.2q-1.4-.1-1.9-1.3c-.5-1.6-.4-2.7-1-2.6-.4 0-.3 1-.7 2.4q-.6 1.3-2 1.6c-6.4.3-10.6 5-10.5 11.1.1 4 .6 6.4 3.4 8.4z"/>
|
||||||
|
<path stroke="none" d="M257.7 242.8H342l-7.5-6.1h-69.4z"/>
|
||||||
|
<path d="m296.4 219.7 1.5 4.6h3.5l-2.8-4.6zm-2 4.6 1 4.6h4l-1.5-4.6zm7 0 2.8 4.6h5.9l-4.6-4.6zm-34.5 10.4c3.1-2.9 5.1-5.3 5.1-8.8h7.6q0 3 1.8 3h7.7v-4.5h-5.6v-24.7c-.2-8.8 10.6-13.8 15-13.8h-26.3v-.8h55.3v.8H301c7.9 0 15.5 7.5 15.6 13.8v7h-1l-.1-6.9c0-6.9-8.7-13.3-15.7-13.1-6 .1-15.4 5.9-15.3 13v2.2l14.3.1-.1 2.5 2.2 1.4 4.5 1.4v3.8l3.2.9v3.7l3.8 1.7v3.8l2.5 1.5-.1 3.9 3.3 2.3h-7.8l4.9 5.5h-7.3l-3.6-5.5h-4.7l2.1 5.4h-5l-1.3-5.4h-6.2v5.8H267zm22.2-15v4.6h5.3l-1-4.6H289z"/>
|
||||||
|
<path fill="none" d="M289.4 211.7h3.3v7.6h-3.3z"/>
|
||||||
|
<path fill="none" d="M284.7 219.8h3.2v-5.6c0-2.4 2.2-4.9 3.2-5 1.2 0 2.9 2.3 3 4.8v5.8h3.4v-14.4h-12.8zm25.6 3.3h4v3.2h-4zm-2.4-5.3h4v3.1h-4zm-3.9-5.4h4v3.1h-4zm-3.3-4.5h4v3.1h-4z"/>
|
||||||
|
<path fill="none" d="m298 219.8 4.2.2 7.3 6.4v-3.8l-2.5-1.8v-3l-3.6-2v-3.3l-3.5-1.2V207l-1.7-1.5z"/>
|
||||||
|
<path d="M315.4 210.3h1v7.1h-1z"/>
|
||||||
|
<g id="af-a">
|
||||||
|
<path d="M257.3 186.5c-1.2-2-2.7 2.8-7.8 6.3-2.3 1.6-4 5.9-4 8.7v5.8c-.1 1.1-1.4 3.8-.5 4.5 2.2 1.6 5.1 5.4 6.4 6.7 1.2 1 2.2-5.3 3-8 1-3 .6-6.7 3.2-9.4 1.8-2 6.4-3.8 6-4.6z"/>
|
||||||
|
<path fill="#bf0000" d="M257 201.9a10 10 0 0 0-1.6-2.6 6 6 0 0 0-2.4-1.8 5 5 0 0 1-2.4-1.5l-.8-1.5v-2l-.3.3c-2.3 1.6-4 5.9-4 8.7v2.3q.2.8.6 1.3l1.1.8 2.7.7a7 7 0 0 1 2.6 2 11 11 0 0 1 1.8 2.6l.2-.8c.8-2.7.7-5.9 2.6-8.5z"/>
|
||||||
|
<path fill="none" d="M249.8 192.4c-.5 3.3 1.4 4.5 3.2 5.1 1.8.7 3.3 2.6 4 4.4m-11.7 1.5c.8 3 2.8 2.6 4.6 3.2s3.7 3 4.5 4.8"/>
|
||||||
|
<path d="m255.6 184.5 1-.6 17.7 29.9-1 .6z"/>
|
||||||
|
<path d="M257.5 183.3a2 2 0 1 1-4 0 2 2 0 1 1 4 0zm15.2-24h7.2v1.6h-7.2zm0 3.1h7.2v13.8h-7.2zm-.4-5h8c.2-2.7-2.5-5.6-4-5.6-1.6.1-4.1 3-4 5.6z"/>
|
||||||
|
<path fill="#bd6b00" stroke="none" d="M292.6 155.8c-1.5.6-2.7 2.3-3.4 4.3s-1 4.3-.6 6.1q.1 1 .5 1.5.3.5.6.5.5 0 .7-.3l.2-.8q-.2-3 .3-5.4a8 8 0 0 1 3-4.4q.4-.3.5-.7l-.3-.7q-.7-.5-1.5-.1m.2.4q.6-.2 1 .1l.1.2-.3.4a8 8 0 0 0-3.1 4.6 17 17 0 0 0-.3 5.6l-.2.6s0 .1-.2 0l-.4-.3-.4-1.2q-.4-2.9.7-6 1.1-2.9 3-4z"/>
|
||||||
|
<path fill="#bd6b00" stroke="none" d="M295.2 157.7q-2.3 1.2-3 4.2a14 14 0 0 0-.3 5.9q.5 2 1.6 2 .5.2.8-.3t.2-1q-.6-2.5-.3-5.1.4-2.6 2.2-4.1.5-.4.5-.8l-.2-.6q-.7-.5-1.5-.2m.2.5q.6-.2 1 0l.1.3-.3.4a7 7 0 0 0-2.4 4.4q-.4 2.8.2 5.2v.8l-.5.1c-.3 0-1-.5-1.2-1.7-.3-1.7-.2-3.9.3-5.7q.8-2.9 2.8-3.8"/>
|
||||||
|
<path d="M272.3 187.4h8v11h-8zm.5 17.4h7.7v2.4h-7.7zm-.2 4.1h8v8.7h-8zm-.6 10.5h8.7v4.9H272zm1.1-16.6h7l1.4-2.4h-9.6zm9.4-8.6.1-6h4.8a17 17 0 0 0-4.9 6z"/>
|
||||||
|
<path fill="none" d="M273.6 196.7c0 1.3 1.5.8 1.5.1v-5.6c0-1 2.4-.8 2.4-.1v6c0 1 1.7.9 1.6 0v-7c0-2.2-5.5-2.1-5.5-.1zm0 13.3h5.7v7h-5.7z"/>
|
||||||
|
<path d="M277.2 213h2v1h-2zm-3.5 0h2v1h-2zm2-3h1.5v3h-1.5zm0 4h1.5v3.1h-1.5zM244 139c.4 5.5-1.4 8.6-4.3 8.1-.8-3 1-5.1 4.3-8.1zm-6.5 12.3c-2.6-1.3-.7-11.5.3-15.8.7 5.5 2 13.3-.3 15.8z"/>
|
||||||
|
<path d="M238.4 151.8c4.4 1.5 8-3.2 9.1-8.7-3.6 5-9.5 5-9 8.7zm-3.3 5.1c-3.4-.9-1.4-11.7-.7-16 .7 4.5 3.1 14.5.7 16zm1.2-.3c.2-3.7 3.9-2.7 6.5-4.7-.5 2-2 5.2-6.5 4.7zm-4.2 5c-3.4-1-1.4-12.6-1.6-17.4 1 4.2 4.2 16.3 1.6 17.4zm1.6-.5c2.8.9 6.5-1 6.8-4.3-2.5 1.7-6.3.4-6.8 4.3z"/>
|
||||||
|
<path d="M229.5 166.7c-3.2.3-1.8-9.6-1.8-18.8 1.2 8.6 4.5 16.5 1.8 18.8z"/>
|
||||||
|
<path d="M230.7 166.3c2.2 1 6.1-.7 7.2-4.4-4 1.7-6.6 0-7.2 4.4zm25.6-22.2c-.6 4.9-2.6 7.7-5.5 7.2-.8-3 1.6-5 5.5-7.2zm-7.8 12.4c4.9.7 6.6-3 10-7.9-4.7 3.4-10.2 4-10 8z"/>
|
||||||
|
<path d="M247 156c-2.6-3.2 0-7.3 2-10.7-.4 5.1 1.3 8-2 10.7zm-1 5.3c-.4-3.2 5-3.9 7.4-5.6-.9 1.8-2 6.7-7.5 5.6z"/>
|
||||||
|
<path d="M244.8 161.3c-3.7-.4-2.2-6.7.5-10.1-1.1 4.8 2 8.1-.5 10.1z"/>
|
||||||
|
<path d="M242 166.6c-4.2-2-1.5-7.2 0-10.3-.6 4.1 2.8 7.2 0 10.2z"/>
|
||||||
|
<path d="M242.8 166c2.2 3 6.5-.8 7.4-5.2-3.7 3.1-6.5 2.6-7.4 5.3zm-9.6 20.3c-.4-4.3 2.8-12 .5-16.2-.3-.6.7-2.1 1.4-1.2 1 1.5 2 5.7 2.5 4.1s.5-4.6 2-5.2c1-.3 2.3-.6 1.9 1-.4 1.4-1.2 3.4-.3 3.5.5 0 2-2 3.3-3 1-.8 2.6.6 1 1.8-4.8 4-9.5 5.9-12.3 15.2zm-8.7 64.5c-.6 0-1.3-.3-.6.6 5.7 7 7.3 9 15.6 8 8.3-1.1 10.3-3.4 16.2-6.7a15 15 0 0 1 11.2-1c1.6.5 2.6.5 1.4-.7s-2.5-2.7-4-3.8a18 18 0 0 0-12.7-2.7c-6 1-11.1 4.9-17.2 6.4a25 25 0 0 1-9.9 0zm47.8 12.5c1 .2 1.7 2.2 2.3.9.8-2.3.2-4-.8-3.9-1.2.3-3.1 3-1.5 3z"/>
|
||||||
|
<path stroke="none" d="M220.6 183q-1.8-2 1-1.9c1.4 0 4.2 1 5.3.1 1-.7.5-3.7 1-5 .2-.9.7-2 2-.2 3.6 5.8 8 12.8 10 19.6 1 3.8 0 9.8-3.4 13.8 0-3.4-1.2-5.7-2.7-8.6-2-3.7-9.1-14-13.2-17.9z"/>
|
||||||
|
<path d="M235.5 213.4c4 0 4.7-5.3 4.7-6.8-2 .4-5.4 3.7-4.7 6.8zm34.5 51.9c2.8.6 2.7-6.2-.2-9.1 1.3 4.4-2 8.4.1 9zm-1.2-.1c.2 3.2-8-.4-10-3 4.8 2.1 9.8.4 10 3zm-3.5-4.6c.3 3.1-7 .3-9.3-2.1 4.9 1.6 9-.5 9.3 2zm1.3.4c2.9.7 2.4-6.4-.4-8.8 1.4 4.7-1.8 8.1.4 8.8zm-3-4.3c2.9.7 1.2-5.4-.9-7.8.4 4.4-1 7.5 1 7.8zm-1.5 0c.3 3.2-5.4.8-7.6-2.3 4.8 1.5 7.3-.3 7.6 2.3zm-1.5-2.5c1.8-1.3-.1-4.8-3.7-4.6.4 2.1 1.6 5.9 3.7 4.6zm14 14.7c.1 3.2-8 1.6-10.6-1.8 5.2 1 10.3-.8 10.5 1.8zm-32.4-5.8c.3 3.2-8.6-.4-10.8-3.4 4.7 1.6 10.5.8 10.8 3.4zm5.4 1.3c1.9-1.3-1.9-4.7-5-5.5.4 2.1 3 6.8 5 5.6zm.6 2.3c.2 2.9-9.5 1.3-12-1.4 8.3 1.5 11.7-1.1 12 1.4z"/>
|
||||||
|
<path d="M252.8 268.6c1 2.7-8.3 2-11.6.5 5.3 0 10.8-2.4 11.6-.5z"/>
|
||||||
|
<path d="M257.1 270.6c1 2.4-7.6 2.4-11.8 1 5.6 0 10.8-3.4 11.8-1zm6.3 1.3c1.6 2.9-7.6 3.1-10.5 1.7 5.2-.7 9.2-4 10.5-1.7zm-10.7-4.9c-2.9 1.8-2.7-3.6-5-7.3 3.6 3.3 7 5.6 5 7.3z"/>
|
||||||
|
<path d="M257.9 269c-2.4 2.1-4.4-5.3-6.6-9.5 3.6 4 8.8 7.7 6.6 9.4zm6.8 2c-2 2.4-8-7-10.2-12 3.3 3.9 11.8 10 10.2 12zm-5.8 7.2c-1 3.6-16.2-3.4-18-7.1 8.8 4.6 18.2 3.6 18 7zm-48.7-73.8c-.4-.5-1.4 0-1.2 1.1.3 1.5 2.5 9.2 6.3 11.8 2.7 2 17 5.1 23.4 6.5q5.3 1 8.9 5.3a94 94 0 0 0-3-9.8c-1.2-3-4.4-6.2-7.8-6.3-6.1-.3-14.1-.8-20-3.3a16 16 0 0 1-6.7-5.3z"/>
|
||||||
|
<path d="M245.5 234.9c2 1.4 4.1-3.7 1.7-8.6-.1 4.7-3.8 6.3-1.7 8.6z"/>
|
||||||
|
<path d="M247.4 239.6c2.7.8 3.5-4 1.8-7.8.3 4.1-4.3 6.6-1.8 7.8z"/>
|
||||||
|
<path d="M249.5 243.4c2.6 1.3 3.5-3.6 1.7-7.1.2 4.5-3.7 5.9-1.7 7z"/>
|
||||||
|
<path d="M248.4 243.7c-1 3-7-2.7-8-5.8 3.7 3.7 8.7 3.2 8 5.7z"/>
|
||||||
|
<path d="M245.7 239c-1.2 3-8.7-5-10.4-8.7 3.7 3.7 11.2 6.5 10.4 8.6z"/>
|
||||||
|
<path d="M244.2 234.3c-1.2 3.5-9.3-5.8-11.7-9.1 4 3.6 12.6 6.6 11.7 9.1zm-.3-3.4c3-.6-.1-3-3.7-6.9-.1 4.1.5 7 3.7 6.9z"/>
|
||||||
|
<path d="M239 228.5c1.3-1.3-1.1-1.9-4.1-5.3-.5 2.3 2.8 6.5 4.2 5.3zm14 15.2c1.6 1 2.6-2.3.7-5.2-.5 3.2-2.1 4-.7 5.2zm-34.2-20.3c-3.3 2-8.6-6-10-9.3 2.9 3.8 10.6 7.2 10 9.3z"/>
|
||||||
|
<path d="M221.7 228c-1.9 2-7.7-3.5-9.7-6.3 3 2.7 10.5 3 9.7 6.3z"/>
|
||||||
|
<path d="M224.8 232.2c-.6 2.8-9-3.5-11-6.5 3.6 3.5 11.6 3.2 11 6.5z"/>
|
||||||
|
<path d="M223.5 235.3c-1.3 2.5-8.2-3.8-9.9-7 4.3 3.6 11 4.5 10 7zM220 223c2.1-2.3 1.2-3.4-.4-7-.8 3.7-2.1 5.2.4 7zm2.9 4.3c4 .2 0-4.6-1-8.7.4 4.6-1 8.3 1 8.7z"/>
|
||||||
|
<path d="M225.4 231.1c2.7-.6 2-4.5-.2-9.2.5 5.1-2.3 8 .2 9.2zm-1 7.7c-1 3-8.8-4-10-6.8 4 3.4 10.7 4.5 10 6.8z"/>
|
||||||
|
<path d="M229.1 243.6c-1.1 3-9.3-3.2-11.8-6.6 4.9 4 12.4 3.6 11.8 6.6z"/>
|
||||||
|
<path d="M233.9 248.5c-1.3 4.3-9.9-2.6-12.4-6 5.4 4.2 13 3 12.4 6zm-8-11c2.3 1.1 3.2-5.4 1.9-10.1 0 5-4.7 8.8-2 10z"/>
|
||||||
|
<path d="M229.8 242.7c2.8.8 2-6.3-.5-11-.3 4.7-2.3 9 .5 11zm5 4.9c3 .1 1-6.1-1.6-9.6.4 4.5-1 9 1.6 9.6zm-5.5 2.6c-1 1.6-3.2-1.3-7-3.5 3.4 1 7.4 2 7 3.5zm-1.8-52.7c3-2.2.7-6.2 0-10-1 3.6-3.4 8.4 0 10zm0 5.3c-4.5-.5-3.8-6.1-4-9.7 1.4 4.9 5 5.7 4 9.8zm.6-.7c3.7-.2 3.5-4.4 3.7-8.6-1.9 3.9-4 4.5-3.7 8.6z"/>
|
||||||
|
<path d="M228 207.3c-3 .3-4.4-2.6-5-7 2.7 4.1 5.1 2.8 5 7zm1-.3c3.7.5 3-3.8 3-7-1.2 3-4.2 4-3 7z"/>
|
||||||
|
<path d="M223.2 205.2c.3 2.8 2.1 7.6 5 6.5 1.1-3.4-2.6-4.1-5-6.5z"/>
|
||||||
|
<path d="M229 212c-1.2-2.4 3-3.7 3.8-6.9.5 4.6.1 7.6-3.8 7zm-11.9-29.2c2.3-2.4.3-6.4-.4-10.2-1 3.6-2.5 8.4.4 10.2zm0 4.6c-4 .5-5-7.7-5.5-11.3 1.4 4.9 6 7 5.5 11.4zm.8 0c2.8-1.5 2.2-4.7 3-7-1.8 2.9-3.6 3.3-3 7z"/>
|
||||||
|
<path d="M217 192.8c-4.1.3-6.6-8.8-6.8-12.4 1.3 4.9 7.4 7.5 6.9 12.4zm.9-.2c4-.9 3.5-3.5 2.9-7.6-1.3 4.2-3.5 3.3-2.9 7.6z"/>
|
||||||
|
<path d="M217 198c-4.6.8-4.3-6.6-8-11.9 3.2 4 9 9 8 11.9zm1-.3c3.6.2 4-5.1 3.8-7.3-.9 2.2-5 4.2-3.7 7.4z"/>
|
||||||
|
<path d="M209.8 192.3c1.7 5.7 4.2 11.4 7.2 11 1.5-3.3-2.9-3.7-7.2-11z"/>
|
||||||
|
<path d="M218.1 202.4c-1.2-2.5 3-3.7 3.8-6.9.5 4.6.1 7.6-3.8 6.9zm-7.1-3.6c2.5 5.1 3.6 11 7 10.1 1.3-4-3.8-4.8-7-10.1z"/>
|
||||||
|
<path d="M218.7 208c-1.5-2.8 2.7-3.7 3.8-7.4.5 4.8 0 8.3-3.8 7.3zm7.2-34.5c2.4.6 5-2.1 4.1-6.2-2.8.6-4 3.2-4.1 6.2zm-7.9-2.1c.2 1.2 1.7 1.3 1.2-.4a5 5 0 0 1 0-3.4 8 8 0 0 0 0-4.6c-.4-1-1.8-.4-1.2.4s.7 2.8.2 3.7q-.7 2.2-.2 4.3zm22.9 16c-1 1.3-2.9.4-1.4-1.5 1.2-1.5 3-2.8 3-4.4.2-2 1.3-5 2.4-6.1s2.4.4 1.2 1.2c-1.3.8-2.2 4.4-2.1 5.8-.1 2-2 3.5-3.1 5zm-3-2.3c-1 1.4-2.4.5-1.6-1.7.7-1.5.8-3.5 1.6-4.6 1.2-1.7 3-3.1 4.1-4.2 1.2-1 2 0 1 1a27 27 0 0 0-3.3 4c-1.4 2.2-.8 4-1.8 5.5zm-15.7-7.2c-.1 2 1.5 2.4 1.4-.4 0-3-2.2-5.8-1-10.3.8-2.2.8-6.3.4-8.4s-2-.8-1.3.9c.6 2-.1 5.6-.6 7.5-1.5 5.4 1.2 8 1 10.7zm4.3-11c-.2 1.9-1.8 2-1.3-.5q.6-2.9 0-5.3c-.6-2.1-.4-5.7 0-7.2.5-1.6 2-.7 1.4.5a10 10 0 0 0-.3 5.9c.6 2 .5 4.8.2 6.7zM210.9 204c.8.9 2 .3 1-1-1-1-.7-1.2-1.3-2.4-.6-1.4-.5-2.1-1.2-3-.7-1-1.6 0-1 .7.8 1 .6 1.6 1 2.5 1 1.5.7 2.3 1.5 3.2zm20.4 24.6a9 9 0 0 1 4.4 6.7 16 16 0 0 0 2 7.1c-2-.5-3-3.7-3.3-6.8-.3-3.2-2-4.5-3-7zm5.1 5.9c1.7 3.1 4 4.3 4.2 6.6.2 2.7.4 2.8 1.1 5.4-2-.5-2.5-.7-3-4.7-.3-2.8-2.6-4.7-2.3-7.3z"/>
|
||||||
|
<path stroke="none" d="M289 263.3c1 1.8 2 4.5 4 4 0-1.3-2.1-2.3-4-4m3 .6c3.7 1.6 7 1.2 7.5 3.6-3.6.4-5-1-7.6-3.6zm-16.1-12.7a14 14 0 0 1 5 7.7 29 29 0 0 0 3.6 7.8 13 13 0 0 1-5.3-7.4c-.7-3-1.6-5.3-3.3-8zm3.1 0c2.8 2.2 5.4 4.8 6.2 7.9.8 2.9 1.3 5.1 3.2 8-3-1.9-4.1-4.7-5-7.8-.7-3-2.5-5.2-4.4-8zm9.2 7.3a1 1 0 0 1 .7-1.2l2.6-.8c1-.3 1.6.4 1.6.9v2q0 .9-.7.9-1.2 0-2.4.7-1 .5-1.5-.5zm10.6 0q0-1-.6-1.2a5 5 0 0 0-2.4-.4q-1.3 0-1.1.6v2.1c0 .8 0 .8.4 1q1.3-.1 2.5.6.9.4 1.1-.6z"/>
|
||||||
|
</g>
|
||||||
|
<use xlink:href="#af-a" width="100%" height="100%" x="-600" transform="scale(-1 1)"/>
|
||||||
|
<g stroke="none">
|
||||||
|
<path d="M328.5 286.6q-.1 1.8 1 3.1a19 19 0 0 0-13.8 1.1c-1.8.8-4-1-1.9-2.7 3-2.3 9.7-1 14.7-1.5m-57.5 0a7 7 0 0 1-.4 3c4.4-1.7 9.1-.2 13.6 1.6 3 1.3 3.3-1 2.8-1.7a7 7 0 0 0-5-2.9zm3.8-21.7q-2-.7-4 1.4c-4.3 4.2-9.4 8.3-13.5 11.6-1.5 1.3-3 3.7 3.4 6 .3.2 5 2 8 2 1.3 0 1.3 1.8 1 2.3-.5 1-.1 1.4-1.1 2.3-1.1 1 0 2.1 1 1.3 3.6-3.2 9.6-1.1 15.3.7 1.4.4 3.8.3 3.8-1.6s1.5-3.4 2.4-3.5c2.4.4 14 .5 17.5.1 2-.3 2.2 2.9 3.3 4 .8.9 3.7 1.1 5.8.2 4-1.8 10-1.8 12.5 0 1 .7 1.9 0 1.3-.7-.8-1-.7-1.6-1.1-2.4-1-2-.2-2.4.8-2.5 11-1.5 14.6-5.2 11.2-8.3-4.4-3.8-9.2-7.7-13.4-12.2-1.2-1.2-2-1.7-4.3-.7a67 67 0 0 1-25.3 5.9 76 76 0 0 1-24.6-5.8z"/>
|
||||||
|
<path fill="#bd6b00" d="m326.6 265.5-1.6.4c-9 3.2-17.2 5.4-25.7 5.4-8.3 0-17-2.4-24.9-5.6a2 2 0 0 0-1.5 0q-.8.2-1.3.7a116 116 0 0 1-11.8 10.3c-.7.5-.6 1.8.5 2.2 8.3 3 16.4 8.5 39.6 8.3 23.5-.2 31.8-5.6 39.2-8.1q.8-.3 1.3-1l.1-.8-.6-.8c-4.3-3.5-8.8-6.3-11.8-10.4q-.5-.7-1.5-.5zm0 .5q.9 0 1.1.3c3 4.3 7.7 7 11.9 10.5l.4.7v.4q-.3.5-1 .7c-7.6 2.6-15.7 8-39 8.2-23.2.2-31.2-5.3-39.5-8.3-.8-.4-.7-1.2-.4-1.4q6.4-4.9 11.8-10.4l1.1-.6h1.2a68 68 0 0 0 25 5.6c8.7 0 17-2.2 26-5.3l1.5-.4z"/>
|
||||||
|
<path d="M269.7 114.6c0-1.4 2-1.5 1.8.4-.3 2.3 4.5 8.3 4.9 12 .3 2.5-1.5 4.6-3.2 6a7 7 0 0 1-6.8.5c-.9-.8-1.7-3.3-1-4.3.2-.3 1.3 3.7 3.7 3.7 3.3 0 6-2.5 6-4.7.2-3.8-5.3-9.8-5.4-13.6m9.5 9.4c.6-.4 1.4 1.3.8 1.7s-1.5-1.3-.8-1.8zm1.5-3.5c-.3.2-.8 0-.7-.2a12 12 0 0 1 3.6-3.3c.4-.2 1 .4.8.7a11 11 0 0 1-3.7 2.8m12.6-10c.3-.6 2.1-1.3 2.6-1.7.4-.5.6.4.4.7-.3.7-1.9 1.7-2.6 1.8q-.6-.1-.4-.7zm4.3.3a8 8 0 0 1 2.5-3.4c.5-.3 1.3 0 1.1.4a9 9 0 0 1-2.9 3.3c-.3.3-.8 0-.7-.3m-3.7 2.7q-.3.5.1.8 1 .3 2 0c.6-.4.3-2.9-.5-1.6-.6.8-1 .6-1.6.8m-7.3 5.6c-1.3-1 .4-2.4 1.7-1.4 2.7 2-4 9.8-7.6 13.4-.7.7-1.3-1-.4-1.9a34 34 0 0 0 6.7-7.6c.4-.5.7-1.6-.4-2.5m15.3-6.6c.1-1-1.6 0-1.6-1.3 0-.7 1.9-1.2 2.7-.4 1.3 1.4.3 3.7-2 3.9-1.8 0-5 2.7-4.5 3.2.5.7 5.4 1.1 8.3.7 1.8-.3 1.4 1.3-.4 1.5s-3.2 0-4.8.6c-2 .5-2.8 3-3.9 4-.2.2-.8-.8-.6-1.2.8-1.2 2-3 3.4-3.6.8-.3-2.4-.4-3.4-.7-.8-.2-.6-1.3-.3-1.9.4-.8 3.4-3.9 4.7-3.8 1.1 0 2.3-.3 2.4-1m5 .2 1.5-1.8c.3-.3.9 0 .8.8-.1.7-1 1.2-1.5 1.7-.5.3-1-.4-.7-.7zm6.5-2.3c.9 0 1 1.6.2 1.8-.6.2-1-1.7-.2-1.8m-2.1 5c0 1.5.7 1.4 2 1.3s2.4 0 2.4-1.2c0-1.3-.7-2.5-1-1.6-.1.8-.3 2.2-.8 1.6-.4-.5-.2-.6-1 .2-.5.5-.5-.2-.8-.6-.2-.3-.8.2-.8.4zm-9.2 7.2c-.3 1.9 0 4.5.9 4.5 1.2 0 3.6-4 4.8-6.2.7-1.2 1.8-1.4 1.3-.1-.7 1.9-.6 6 0 7.2.4.6 3-.6 3.4-1.5.8-1.7.1-4.8.4-6.7.1-1.2 1.3-1.5 1.2-.3l-.1 7.5c0 1 2.9 2.4 3.3-.6.2-1.8 1.2-3.7 0-5.7-.8-1.3 1.1-1.2 2.1.6.7 1.2-.6 3.2-.5 4.7 0 2.4-1.8 3.8-3.1 3.8-1.2 0-2-1.5-3-1.5s-2.2 1.7-3 1.6c-3.6-.2-1.7-5.3-2.8-5.4-1.2 0-2.5 5-4 4.9-1.4-.2-3-4.2-2.3-5.8.5-1.6 1.5-2 1.4-1m16.9-8c-1.7-1 0-3.7.9-2.8 1.6 2 3.2 6.5 4.4 6.9.7.2.6-3.4 1.1-5 .4-1.3 1.8-.9 1.6.7-.1.5-2 6.4-1.8 6.6a47 47 0 0 1 3.3 7.8c.3 1.2-1.1.4-1.3.2-.9-1.4-2.4-6.5-2.4-6.2l-1.7 7.7c-.2 1-1.7.8-1.3-1 .3-1.4 2.3-8.3 2.2-8.6a17 17 0 0 0-5-6.3"/>
|
||||||
|
<path d="M322 131.2c-.4 0-1.2 1 1.2 1.5 3.1.6 6.6-.5 7.6-3.6 1.3-3.7 2-7.2 2.7-8.5.8-1.5 1.8-1.4 1-3.6-.5-1.7-1.5-1.2-1.7-.3-.5 2.3-2.6 10-3.3 11.3q-1.8 3.8-7.5 3.2"/>
|
||||||
|
<path d="M328.4 119c-.4-.7-1.2 0-1 .7a1 1 0 0 0 1.2 1c.7 0 2.2.1 2.2-1 0-.8-.7-1.5-1.1-.6q-.8 1.1-1.3 0zm.7-3c-.2.2 0 1.1.3 1a7 7 0 0 0 3.3-.8c.2-.2.1-.7-.2-.7-1 0-2.6 0-3.4.5m8.8 2.3c.8-1.2 2.8-1.3 2 .4l-6.3 12.3c-.8 1.4-1.4.7-.8-.4.7-1.4 4.9-12 5.1-12.3"/>
|
||||||
|
<path d="M330.2 133c-.2-.8-1.5-2-1.3.2.2 3.8 5.5 2.6 7 1.3s.3 4.3 2.2 4.9c1 .3 3-1.1 4-2.4 2.7-3.5 4.5-8.6 7-12 1-1.4-.5-2.4-1-1.3-2.4 3.8-5.2 11.6-8.3 13.6-2.5 1.6-1.7-2-1.8-3.2-.1-.8-1.1-2-2.4-.9a6 6 0 0 1-3.7 1.2c-.7 0-1.4 0-1.7-1.4"/>
|
||||||
|
<path d="M339.6 126c0-.3-1.1-.4-1 .7 0 .8 1 1 1.1 1 1.5-1.2-.3-.6-.1-1.8zm-2.3 4.4c-.3 0-.6 1 .2 1.1l3.9-.2c.4 0 .6-.9-.4-.8-1.2 0-2.7-.3-3.7 0zm-62-16.6c.5 0 1.6 1.4 1.5 1.9 0 .2-1.2 0-1.5-.3s-.2-1.6 0-1.6m-5.3 10.4c-1 .6.2 1.7 1 1.2 2.8-1.9 7-3.8 8-7.5.3-1.2 1.4-3.1 2.5-3.5 1-.5 2.6 1.9 3.6 0 .6-1 2.7.7 3.2-.4.6-1.3.3-2 .3-3.4 0-.8-.7-1-1.2.3l-.1 1.6q-.4.4-1 .2c-.2-.2 0-.7-.6-1q-.4-.1-.8.2c-.7 1.3-1 2.5-2.1 1-.9-1-1.4-3.1-2-.3-.2 1-1.7 2.4-2.6 2.4-1.1 0-.8-3-3.2-2.5-1.3.3-1.2 2.7-1 3.5.3 1.3 4 .4 3.7 1.2-.6 2.7-4.4 5.4-7.7 7m-22.7 13.2c-.1.5.5 1.7 1.1 1.8.6 0 1-1.3.8-1.8-.2-.3-1.8-.3-1.9 0m3.3 4.9c-.4-.4-1.6.7-.6 1.5.5.5 2.5 1.1 3 .2.8-1.2-.7-5.5 0-6 .5-.5 2.8 2.8 4 3 2.7.4 2-4.6 5-4.2 1.9.2 2.1-2.2 1.8-3.8-.2-1.5-2.6-3.6-3.7-4.6-1.4-1.2-2.1 1-1.2 1.6 1.2 1 3.3 2.9 3.6 4.1.1.6-1.4 1.8-2 1.5-1.4-.8-2.6-4-3.8-4.7-.4-.2-1.4.3-1 1.3.6 1.1 3 2.7 3.1 3.9.1 1-1 3.2-1.8 3.2s-3-2.7-3.7-4c-.4-.5-1.5-.5-1.7.4a22 22 0 0 0 .5 5.5c.2 1.6-.9 1.7-1.5 1.1m-4-8.6c-.4.4.8 1.2 1 1 .4-.4 2.1-2.3 1.8-3-.3-.6-2.6-2-3-1.3-.7 1.1 2.2 1.7 1.7 2zm4.1-8.4s.8 2.5 1.4 1.4c.4-.7-1.4-1.4-1.4-1.4m1.2 4c-.2 0-1 .7-.5 1 .8.4 2.9.8 2.4-.7-.3-.9 3.2 0 2.3-2.4a4 4 0 0 0-1.7-1.7c-.4 0-1.5.5-.8.9.5.2 2 1.1 1.5 1.7-.7.6-1.1-.3-1.9-.1-.4 0-.1 1.2-.4 1.5 0 .2-.7-.4-.9-.3zm5.5-9.5a4 4 0 0 0-1.2 2q.1.5.5.5a3 3 0 0 0 1.2-1.9c0-.3-.2-.8-.5-.6m2.8-.3c-.8-1 1-2.6 1.7-.5.5 1.3 5.5 7.9 6.5 10.1.8 1.5 0 2.1-.9 1-2.5-3.2-4.6-7.2-7.3-10.6m5.2.1c.9-1 2.7-3 2.2-4s-1.5-1-1.7-.7c-1 1.3.8 1 .5 1.4q-.8 1.3-1.3 2.6c-.1.3.1.9.3.7m77.8 3.2c-.7-.5.6-3 1.5-2 2.3 2.7 3.4 11.6 4.1 18.3 0 0-1 .9-1 .7 0-3.5-1.5-14.4-4.6-17m-53.1-8.6c-.8-1.8 1.1-2.4 1.4-1.2 1.3 5.8 4.5 10.2 7 14.1.7 1.2 0 2-1.7.8-1.2-.8-2.5-3.9-3-4-1.2-.2-3.8 5-9.1 3.5-1.4-.4-1.3-4.5-1.4-6.3 0-.9 1-1 1 0 0 1.7 0 5.2 2.1 5.4 1.8 0 5.6-2.4 6.4-4.4s-1.9-5.9-2.7-8z"/>
|
||||||
|
<path d="M344.6 138.4c.4-1.2 6.1-10.8 6.9-12.9.4-1 2 1.8.4 3.3-1.4 1.2-5.5 8-6.3 10.4-.4 1-1.4.5-1-.8"/>
|
||||||
|
<path d="M354.3 129.3c1-4 3.6.6 1.3 2.8-3.4 3.4-4.5 9.9-10 10.9-1.4.3-4-.7-4.8-1.3-.3-.2.2-1.6 1.1-.9 1.3 1 4.1 1.3 5.6.1a25 25 0 0 0 6.8-11.6m-57 12.7c-.3.3-1 .3-1.1.7-.3 1.4 0 2.2-.3 3.6s-1.3 1.4-1.2.3c0-1.4 1.3-3.5.4-3.6-.6-.1-1-.9-.4-1.3q1.5-.9 2.4-.4.5.3.2.7"/>
|
||||||
|
<path d="M296.5 140c-1.4 1.4-2.8 1.9-4.1 3.5-.6.6-.5 1.5-.9 2.4-.3.9-1.4 1-1.7.9-.5-.4-.4-2-1-1.2s-.9 2-1.7 2-2-1.5-1.3-1.5c2.3-.3 2.2-2 3-2.2 1-.1 1 1.5 1.7 1.2.4-.2.7-2.1 1.2-2.6 1.5-1.6 2.7-2.4 4.3-3.6.7-.6 1.3.5.5 1.2zm5.3 5c-1.2.2-1 1.7-.6 1.8.5.3 1.4.4 1.7-1.3.2-.7.3 3.5 1.8 1.9 1-1 3.1.2 4-1 .7-.9 1-1.5.4-2.7-.2-.3-1-.2-1 .7s-.5 1.7-1.3 1.6c-.4-.1.2-1.9-.2-2.4h-.7c-.3.4.3 2.2-.6 2.4-1.2.2-.6-1.2-1-1.4-1.7-.8-1.8.2-2.5.3zm9-3c.9-.2.6-.2 2-1.3.5-.4.6.8.5 1.3 0 .7-1 .2-1.3.9-.4.9-.2 3-.4 3.8 0 .4-.8.4-.8 0-.2-1 .1-2 0-3.3 0-.4-.5-1.1 0-1.3zm-5-2.5-.2 2.3c0 .5 1 .2 1 .1 0-.8.2-2 0-2.3q-.5-.3-.8-.1"/>
|
||||||
|
<path d="m299.5 130.2-1.4 5.6-2-3.8v3.9l-4.4-5.2 1.5 5.6-4-3.4 2.2 3.8-7-4.5 4.4 5.2-5.6-2.8 4 3.4-9-3.4 8.7 4.3a29 29 0 0 1 12.6-2.6q7.5.1 12.5 2.6l8.8-4.3-9 3.4 4-3.4-5.5 2.8 4.3-5.2-7 4.5 2.2-3.8-4 3.3 1.5-5.5-4.3 5.2V132l-2 3.8z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path fill="#fff" d="m311.3 295-.3 2.6h-.4l-.1-1.8-.5-1.6-.5-1.3-1-1.4.8-2.2a7 7 0 0 1 1.5 2.4 9 9 0 0 1 .5 3.2m7-4.2q0 1-.5 1.5-.3.5-1.3.7l.4 1.5v2l-.1 1.3h-.4l-.1-1.3-.2-1-.4-1-.7-1.4-1-1.7.6-2 1 1q.4.3 1 .3 1.2 0 1.2-1.3h.4v1.4m6.4 4.8-.5 2.1q-.6 0-.8-.7l-.4-1.3-.1-1.7-1 .2a2 2 0 0 1-1.3-.4 1 1 0 0 1-.5-1q0-1.4.7-2.3.8-1 1.5-1.1.7 0 1 .4l.3.9v2q0 1.3.3 1.9 0 .4.8 1m-2-3.5q0-.9-.8-.8l-.6.1q-.2.1-.2.3 0 .5 1 .5zm8.7 3-.3 2.6q-.8-.5-1.4-2l-1.3-4.1-1.8 5.5-.8.7v-2.5q.9-1 1-1.5l.8-1.7.5-2.7h.4l.9 2.7q.3 1 .9 1.6l1 1.4"/>
|
||||||
|
<path fill="#bf0000" d="M350.8 319.4q.6.6.7 1.2l.4 1.6-.8.1-1-1.5-1.1-1.2-1.7-1.5-2-1.7q-.6-.3-.6-.5l-.3-.8-.2-1.6 2.7 2.2 2.5 2.2zm-9.5-5.8-.2 2H338l.3-2zm8.4 8.9-7.6 2.3-1.3-2 6.5-2-.7-.8-.9-.6a1 1 0 0 1-.4 1l-1 .6a3 3 0 0 1-1.8 0 2 2 0 0 1-1.3-.7 4 4 0 0 1-.7-2.2q0-1.5.9-1.8 1.1-.3 3 .7a8 8 0 0 1 3 2.4zm-5.8-4-.8-.3h-.6l-.5.3v.6l.5.2h.6l.4-.3zm-8-1.6-.5 2-3.2-.3.5-2zm7.5 7.7-1.7.4H340l-1.5-.4q-.5.8-1.5 1.2l-1.6.6-1.2.3-1-2 1.1-.3 1.3-.4.9-.5-1-.5h-.9l-.2.3h-.5q-.8-1.2-.3-2c.5-.8.9-.8 2-1a7 7 0 0 1 2.6-.2q1.2.1 1.5.9.2.3.2.7l-.4 1.2h1.1l1.7-.3zm-8 1.8-1.6.3a3 3 0 0 1-2.2-.4 6 6 0 0 1-1.7-2.6l-.8-2.2a2 2 0 0 0-.8-1l-.9-.5.6-2.1q.9.4 1.4 1l1 1.7.5 1.5 1.1 2.2q.5.4 1 .3l1.7-.2zm-7-7.5-1 1.9-3-.7 1-1.9zm1.8 8.4-7.5.7-.4-2 6.2-.7-.6-.8-1-.6.5-2q1 .6 1.6 1.3.5.8.8 2.1zm-6 1-2.2-.2-1.7-.5-1.3.4h-3.7l-1.2-.3q-.4-.3-.8-1a4 4 0 0 1-1.5 1l-1.7.1h-1.7l.2-2.1h1.7q1.2.1 2.1-.4a2 2 0 0 0 1.3-1.8l.7.1-.1 1.3q0 .4.3.7.4.3 1 .3h1.5q1.5 0 2-.2.9-.2 1-1.1l.1-.4s.3 0 .5-.2l.5-.2v.7l-.3 1.1 2 .5q.1-.3-.1-.7l-.3-.6.1-.3.3-.2 1-.9.5 1v1zm-11.3-8.7-2 1.3-1.3-.9-1.4 1-1.9-1 1.8-1.3 1.5.8 1.5-1 1.8 1m-3 8.2-7.3-1.2.8-2 6.2 1q0-.6-.2-1l-.5-.8 1.6-1.7q.6.8.7 1.6t-.5 2.1zm-6.1-1-1.6-.3q-1.3-.3-1.5-1.2-.3-.9.8-2.8l1.2-2q.4-.7.3-1.2l-.3-.7 2.2-1.6q.4.8.3 1.4 0 .8-.7 1.8l-.8 1.4a6 6 0 0 0-.9 2.2q0 .6.5.7l1.6.4zm-3.8-8-2.5 1.1-1.8-1.7 2.6-1zm-1 6.6-1.6 1.4-1.7.6-2.4-.1-2.8-.7a8 8 0 0 1-3.4-2q-.9-1.2 0-2.2a7 7 0 0 1 2-1.6q1.1-.7 3.8-1.6l.4.5-2.8 1.2q-.8.4-1.3 1t.2 1.6a11 11 0 0 0 6.3 2.2q1.8 0 2.3-.7.4-.4.5-1l.2-1.6 2.5-1.5-.1 1.5a4 4 0 0 1-1 1.6z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ag" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="ag-a">
|
||||||
|
<path fill-opacity=".7" d="M-79.7 0H603v512H-79.7z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g fill-rule="evenodd" clip-path="url(#ag-a)" transform="translate(74.7)scale(.9375)">
|
||||||
|
<path fill="#fff" d="M-79.7 0H603v512H-79.7z"/>
|
||||||
|
<path fill="#000001" d="M-79.6 0H603v204.8H-79.7z"/>
|
||||||
|
<path fill="#0072c6" d="M21.3 203.2h480v112h-480z"/>
|
||||||
|
<path fill="#ce1126" d="M603 .1V512H261.6L603 0zM-79.7.1V512h341.3L-79.7 0z"/>
|
||||||
|
<path fill="#fcd116" d="M440.4 203.3 364 184l64.9-49-79.7 11.4 41-69.5-70.7 41L332.3 37l-47.9 63.8-19.3-74-21.7 76.3-47.8-65 13.7 83.2L138.5 78l41 69.5-77.4-12.5 63.8 47.8L86 203.3z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 743 B |
@@ -0,0 +1,29 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-ai" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<path id="ai-b" fill="#f90" d="M271 87c1.5 3.6 6.5 7.6 7.8 9.6-1.7 2-2 1.8-1.8 5.4 3-3.1 3-3.5 5-3 4.2 4.2.8 13.3-2.8 15.3-3.4 2.1-2.8 0-8 2.6 2.3 2 5.1-.3 7.4.3 1.2 1.5-.6 4.1.4 6.7 2-.2 1.8-4.3 2.2-5.8 1.5-5.4 10.4-9.1 10.8-14.1 1.9-.9 3.7-.3 6 1-1.1-4.6-4.9-4.6-5.9-6-2.4-3.7-4.5-7.8-9.6-9-3.8-.7-3.5.3-6-1.4-1.6-1.2-6.3-3.4-5.5-1.6"/>
|
||||||
|
</defs>
|
||||||
|
<clipPath id="ai-a">
|
||||||
|
<path d="M0 0v120h373.3v120H320zm320 0H160v280H0v-40z"/>
|
||||||
|
</clipPath>
|
||||||
|
<path fill="#012169" d="M0 0h640v480H0z"/>
|
||||||
|
<path stroke="#fff" stroke-width="50" d="m0 0 320 240m0-240L0 240"/>
|
||||||
|
<path stroke="#c8102e" stroke-width="30" d="m0 0 320 240m0-240L0 240" clip-path="url(#ai-a)"/>
|
||||||
|
<path stroke="#fff" stroke-width="75" d="M160 0v280M0 120h373.3"/>
|
||||||
|
<path stroke="#c8102e" stroke-width="50" d="M160 0v280M0 120h373.3"/>
|
||||||
|
<path fill="#012169" d="M0 240h320V0h106.7v320H0z"/>
|
||||||
|
<path fill="#fff" d="M424 191.8c0 90.4 9.7 121.5 29.3 142.5a179 179 0 0 0 35 30 180 180 0 0 0 35-30c19.5-21 29.3-52.1 29.3-142.5-14.2 6.5-22.3 9.7-34 9.5a78 78 0 0 1-30.3-9.5 78 78 0 0 1-30.3 9.5c-11.7.2-19.8-3-34-9.5"/>
|
||||||
|
<g transform="matrix(1.96 0 0 2.002 -40.8 62.9)">
|
||||||
|
<use xlink:href="#ai-b"/>
|
||||||
|
<circle cx="281.3" cy="91.1" r=".8" fill="#fff" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(-.916 -1.77 1.733 -.935 563.4 829)">
|
||||||
|
<use xlink:href="#ai-b"/>
|
||||||
|
<circle cx="281.3" cy="91.1" r=".8" fill="#fff" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(-1.01 1.716 -1.68 -1.031 925.4 -103.2)">
|
||||||
|
<use xlink:href="#ai-b"/>
|
||||||
|
<circle cx="281.3" cy="91.1" r=".8" fill="#fff" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#9cf" d="M440 315.1a78 78 0 0 0 13.3 19.2 179 179 0 0 0 35 30 180 180 0 0 0 35-30 78 78 0 0 0 13.2-19.2z"/>
|
||||||
|
<path fill="#fdc301" d="M421.2 188.2c0 94.2 10.2 126.6 30.6 148.5a187 187 0 0 0 36.5 31.1 186 186 0 0 0 36.4-31.1c20.4-21.9 30.6-54.3 30.6-148.5-14.8 6.8-23.3 10.1-35.5 10-11-.3-22.6-5.7-31.5-10-9 4.3-20.6 9.7-31.5 10-12.3.1-20.7-3.2-35.6-10m4 5c14 6.5 22 9.6 33.5 9.4a76 76 0 0 0 29.6-9.4c8.4 4 19.3 9.2 29.6 9.4 11.5.2 19.4-3 33.4-9.4 0 89-9.6 119.6-28.8 140.2a176 176 0 0 1-34.2 29.4 176 176 0 0 1-34.3-29.4c-19.2-20.6-28.7-51.3-28.7-140.2z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-al" viewBox="0 0 640 480">
|
||||||
|
<path fill="red" d="M0 0h640v480H0z"/>
|
||||||
|
<path id="al-a" fill="#000001" d="M272 93.3c-4.6 0-12.3 1.5-12.2 5-13-2.1-14.3 3.2-13.5 8q2-2.9 3.9-3.1 2.5-.3 5.4 1.4a22 22 0 0 1 4.8 4.1c-4.6 1.1-8.2.4-11.8-.2a17 17 0 0 1-5.7-2.4c-1.5-1-2-2-4.3-4.3-2.7-2.8-5.6-2-4.7 2.3 2.1 4 5.6 5.8 10 6.6 2.1.3 5.3 1 8.9 1s7.6-.5 9.8 0c-1.3.8-2.8 2.3-5.8 2.8s-7.5-1.8-10.3-2.4c.3 2.3 3.3 4.5 9.1 5.7 9.6 2 17.5 3.6 22.8 6.5a37 37 0 0 1 10.9 9.2c4.7 5.5 5 9.8 5.2 10.8 1 8.8-2.1 13.8-7.9 15.4-2.8.7-8-.7-9.8-2.9-2-2.2-3.7-6-3.2-12 .5-2.2 3.1-8.3.9-9.5a274 274 0 0 0-32.3-15.1c-2.5-1-4.5 2.4-5.3 3.8a50 50 0 0 1-36-23.7c-4.2-7.6-11.3 0-10.1 7.3 1.9 8 8 13.8 15.4 18s17 8.2 26.5 8c5.2 1 5.1 7.6-1 8.9-12.1 0-21.8-.2-30.9-9-6.9-6.3-10.7 1.2-8.8 5.4 3.4 13.1 22.1 16.8 41 12.6 7.4-1.2 3 6.6 1 6.7-8 5.7-22.1 11.2-34.6 0-5.7-4.4-9.6-.8-7.4 5.5 5.5 16.5 26.7 13 41.2 5 3.7-2.1 7.1 2.7 2.6 6.4-18.1 12.6-27.1 12.8-35.3 8-10.2-4.1-11 7.2-5 11 6.7 4 23.8 1 36.4-7 5.4-4 5.6 2.3 2.2 4.8-14.9 12.9-20.8 16.3-36.3 14.2-7.7-.6-7.6 8.9-1.6 12.6 8.3 5.1 24.5-3.3 37-13.8 5.3-2.8 6.2 1.8 3.6 7.3a54 54 0 0 1-21.8 18c-7 2.7-13.6 2.3-18.3.7-5.8-2-6.5 4-3.3 9.4 1.9 3.3 9.8 4.3 18.4 1.3s17.8-10.2 24.1-18.5c5.5-4.9 4.9 1.6 2.3 6.2-12.6 20-24.2 27.4-39.5 26.2-6.7-1.2-8.3 4-4 9 7.6 6.2 17 6 25.4-.2 7.3-7 21.4-22.4 28.8-30.6 5.2-4.1 6.9 0 5.3 8.4-1.4 4.8-4.8 10-14.3 13.6-6.5 3.7-1.6 8.8 3.2 9 2.7 0 8.1-3.2 12.3-7.8 5.4-6.2 5.8-10.3 8.8-19.9 2.8-4.6 7.9-2.4 7.9 2.4-2.5 9.6-4.5 11.3-9.5 15.2-4.7 4.5 3.3 6 6 4.1 7.8-5.2 10.6-12 13.2-18.2 2-4.4 7.4-2.3 4.8 5-6 17.4-16 24.2-33.3 27.8-1.7.3-2.8 1.3-2.2 3.3l7 7c-10.7 3.2-19.4 5-30.2 8l-14.8-9.8c-1.3-3.2-2-8.2-9.8-4.7-5.2-2.4-7.7-1.5-10.6 1 4.2 0 6 1.2 7.7 3.1 2.2 5.7 7.2 6.3 12.3 4.7 3.3 2.7 5 4.9 8.4 7.7l-16.7-.5c-6-6.3-10.6-6-14.8-1-3.3.5-4.6.5-6.8 4.4 3.4-1.4 5.6-1.8 7.1-.3 6.3 3.7 10.4 2.9 13.5 0l17.5 1.1c-2.2 2-5.2 3-7.5 4.8-9-2.6-13.8 1-15.4 8.3a17 17 0 0 0-1.2 9.3q1.1-4.6 4.9-7c8 2 11-1.3 11.5-6.1 4-3.2 9.8-3.9 13.7-7.1 4.6 1.4 6.8 2.3 11.4 3.8q2.4 7.5 11.3 5.6c7 .2 5.8 3.2 6.4 5.5 2-3.3 1.9-6.6-2.5-9.6-1.6-4.3-5.2-6.3-9.8-3.8-4.4-1.2-5.5-3-9.9-4.3 11-3.5 18.8-4.3 29.8-7.8l7.7 6.8q2.3 1.5 3.8 0c6.9-10 10-18.7 16.3-25.3 2.5-2.8 5.6-6.4 9-7.3 1.7-.5 3.8-.2 5.2 1.3 1.3 1.4 2.4 4.1 2 8.2-.7 5.7-2.1 7.6-3.7 11s-3.6 5.6-5.7 8.3c-4 5.3-9.4 8.4-12.6 10.5-6.4 4.1-9 2.3-14 2-6.4.7-8 3.8-2.8 8.1 4.8 2.6 9.2 2.9 12.8 2.2 3-.6 6.6-4.5 9.2-6.6 2.8-3.3 7.6.6 4.3 4.5-5.9 7-11.7 11.6-19 11.5-7.7 1-6.2 5.3-1.2 7.4 9.2 3.7 17.4-3.3 21.6-8 3.2-3.5 5.5-3.6 5 1.9-3.3 9.9-7.6 13.7-14.8 14.2-5.8-.6-5.9 4-1.6 7 9.6 6.6 16.6-4.8 19.9-11.6 2.3-6.2 5.9-3.3 6.3 1.8 0 6.9-3 12.4-11.3 19.4 6.3 10.1 13.7 20.4 20 30.5l19.2-214L320 139c-2-1.8-8.8-9.8-10.5-11-.7-.6-1-1-.1-1.4s3-.8 4.5-1c-4-4.1-7.6-5.4-15.3-7.6 1.9-.8 3.7-.4 9.3-.6a30 30 0 0 0-13.5-10.2c4.2-3 5-3.2 9.2-6.7a86 86 0 0 1-19.5-3.8 37 37 0 0 0-12-3.4zm.8 8.4c3.8 0 6.1 1.3 6.1 2.9s-2.3 2.9-6.1 2.9-6.2-1.5-6.2-3c0-1.6 2.4-2.8 6.2-2.8"/>
|
||||||
|
<use xlink:href="#al-a" width="100%" height="100%" transform="matrix(-1 0 0 1 640 0)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-am" viewBox="0 0 640 480">
|
||||||
|
<path fill="#d90012" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="#0033a0" d="M0 160h640v160H0z"/>
|
||||||
|
<path fill="#f2a800" d="M0 320h640v160H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ao" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd" stroke-width="1pt">
|
||||||
|
<path fill="red" d="M0 0h640v243.6H0z"/>
|
||||||
|
<path fill="#000001" d="M0 236.4h640V480H0z"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#ffec00" fill-rule="evenodd" d="M228.7 148.2c165.2 43.3 59 255.6-71.3 167.2l-8.8 13.6c76.7 54.6 152.6 10.6 174-46.4 22.2-58.8-7.6-141.5-92.6-150z"/>
|
||||||
|
<path fill="#ffec00" fill-rule="evenodd" d="m170 330.8 21.7 10.1-10.2 21.8-21.7-10.2zm149-99.5h24v24h-24zm-11.7-38.9 22.3-8.6 8.7 22.3-22.3 8.7zm-26-29.1 17.1-16.9 16.9 17-17 16.9zm-26.2-39.8 22.4 8.4-8.5 22.4-22.4-8.4zM316 270l22.3 8.9-9 22.2-22.2-8.9zm-69.9 70 22-9.3 9.5 22-22 9.4zm-39.5 2.8h24v24h-24zm41.3-116-20.3-15-20.3 14.6 8-23-20.3-15h24.5l8.5-22.6 7.8 22.7 24.7-.3-19.6 15.3z"/>
|
||||||
|
<path fill="#fe0" fill-rule="evenodd" d="M336 346.4c-1.2.4-6.2 12.4-9.7 18.2l3.7 1c13.6 4.8 20.4 9.2 26.2 17.5a8 8 0 0 0 10.2.7s2.8-1 6.4-5c3-4.5 2.2-8-1.4-11.1-11-8-22.9-14-35.4-21.3"/>
|
||||||
|
<path fill="#000001" fill-rule="evenodd" d="M365.3 372.8a4.3 4.3 0 1 1-8.7 0 4.3 4.3 0 0 1 8.6 0zm-21.4-13.6a4.3 4.3 0 1 1-8.7 0 4.3 4.3 0 0 1 8.7 0m10.9 7a4.3 4.3 0 1 1-8.7 0 4.3 4.3 0 0 1 8.7 0"/>
|
||||||
|
<path fill="#fe0" fill-rule="evenodd" d="M324.5 363.7c-42.6-24.3-87.3-50.5-130-74.8-18.7-11.7-19.6-33.4-7-49.9 1.2-2.3 2.8-1.8 3.4-.5 1.5 8 6 16.3 11.4 21.5A5288 5288 0 0 1 334 345.6c-3.4 5.8-6 12.3-9.5 18z"/>
|
||||||
|
<path fill="#ffec00" fill-rule="evenodd" d="m297.2 305.5 17.8 16-16 17.8-17.8-16z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width="3" d="m331.5 348.8-125-75.5m109.6 58.1L274 304.1m18.2 42.7L249.3 322"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-aq" viewBox="0 0 640 480">
|
||||||
|
<path fill="#3a7dce" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fff" d="M157.7 230.8c-3.5-7.8-3.5-7.8-3.5-15.6-1.8 0-2 .3-3 0-1.1-.3-1.5 7.2-4.8 5.8-.5-.8 2.4-6.2-.7-8.5-1-.7.2-5.2-.2-7.2 0 0-4 2.4-7-5.8-1.5-2.2-3.5 2-3.5 2s.9 2.4-.7 3c-2.2-1.8-3.9-.8-6.7-3.4s.6-5.4-4.8-7.5c3.5-9.8 3.5-7.9 12.2-11.8-5.2-4-5.2-4-8.7-9.8-5.2-2-7-4-12.2-7.8-7-9.9-10.5-29.5-10.5-43.2 4.4-4.6 10.5 15.7 19.2 21.6l12.2 5.9c7 3.9 8.7 7.8 14 11.7l15.6 6c7 5.8 10.5 13.6 15.7 15.6 5.7 0 6.8-3.7 8.6-3.9 10.3-.6 15.5-2 17.5-5.5 2.1-2.8 7 1.6 21-4.3l-1.7-7.9s3.7-3.4 8.7-2c-.1-3.5-.5-13 4.5-17.4-3-3.5 1.8-9 2-10.7-1.4-8.6 1.4-8.7 2-11.3.6-2.5-2.4-1.7-1.6-5.2.9-3.5 6-4.3 6.6-7.2.7-2.9-1.1-14.3-1.3-16.8 9.4-2.8 12.4-11.4 15.7-7.8C264 70 265.8 66 276.3 66c1.4-3.6-3.9-6.7-1.8-7.9 3.5-.5 6.1-.2 10.2 5.7 1.3 2 1.6-2.7 2.9-3.2s4.4-.5 4.9-2.8c.5-2.4 1.2-5.6 3-9.5 1.4-3.2 2.5 1.3 3.8 7.5 7.4.3 24 2.1 31 4.3 5.2 1.5 8.7-1.5 13.7-2.2 3.7 4.2 7.2 1 9.2 10 2.7 4.8 7.3.4 8.3 1.8 5.8 18.1 25.8 5.9 27.4 6.2 2.5 0 5.6 8 7.7 7.9 3.2-.6 2.3-3.1 5.2-2.1-.8 6.8 5.6 14.6 5.6 19.7 0 0 1.5.9 3-.6 1.4-1.6 2.7-5.4 4-5.3 3 .5 22 6 25.8 7.9 1.7 3.5 3.3 5.3 6.8 4.7 2.8 2.1.8 5 2.4 5.1 3.5-2 4.7-4 8.2-2.1 3.5 2 7 5.9 8.7 9.8 0 2-1.8 9.8 0 21.6.9 3.9 9.7 32.3 9.7 35.2 0 4-2.7 6-4.5 9.9 7 5.9 0 15.7-3.5 21.6 26.2 5.9 14 17.6 34.9 11.7-5.2 13.8-3.4 12.7 1.8 26.4-10.4 7.8-.2 10.2-7.1 20-.5.7 4.1 8.6 10.5 8.6-1.7 15.6-7 9.8-5.2 33.3-13.7-.3-8.2 17.6-17.4 15.7.5 11.2 5.2 12.2 3.4 23.5-7 2-7 2-10.4 7.9l-5.2-2c-1.8 9.8-5.3 11.8 0 21.6 0 0-6.8.2-8.8 0-.1 3.4 3 4.3 3.5 7.8-.2 1.4-9.9 7.6-17.4 7.9-2 4.8 5.2 10 4.8 12.4-8.2 1.8-11.8 13-11.8 13s4.2 2 3.5 4c-2.2-1.8-3.5-2-7-2-1.7.5-6 0-10 7.7-4.5 1.6-6.6 1-10 6-1.5-4.7-3.7.1-6.3 2-2.7 1.8-6.2 6.5-6.7 6.3.1-1.4 1.6-6.3 1.6-6.3L399 437c-.7.1-.5-5.7-2.2-5.5s-6.4 7.3-8 7.5-2.1-2.2-3.5-2-4 7.5-5 7.7c-1 .1-5-4.5-8.3-3.8-17.1 6.8-19.9-13.4-22.5-2-3.6-2.2-3-1-6.7.1-2.3.7-2.5-3.4-4.6-3.4-4.1.2-4 4.6-6.2 3.3-1.8-9.2-13-7.6-14-11.5s4.8-4 6.6-6.8c1.4-4-1.5-5.6 4.3-9.4 7.5-5.7 6.8-19.8 4.9-25.3 0 0-5.9-17.7-7-17.7-3.5-1-3.5 6.5-8.6 8.6-10.5 4-29-9.9-32.2-9.9-2.9 0-16.5 3.6-16-4-2 7.4-9.5 1.7-10 1.7-7 0-4.3 6.1-9 5.9-2.1-.8-23.6-2.3-23.6-2.3v4l-26.1-11.8c-10.5-4-5.3-13.7-22.7-7.8v-11.8h-8.7c3.5-23.6 0-11.8-1.8-33.4l-7 2c-7-10.6 9.8-8.6-5.2-15.7 0 0 .3-11.7-3.5-7.8-.7.5 1.8 5.8 1.8 5.8-14-2-17.4-5.8-17.4-21.5 0 0 11.4 1.8 10.4 0-1.6-3-3.7-22-3.4-23.4-.1-2.6 10.7-9 8.6-15.2 1.4-.6 5.3-.7 5.3-.7"/>
|
||||||
|
<path fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="2.5" d="M595.5 297.6q-.9 2 .1 3.6c1.1-1.7.2-2.4 0-3.6zm-476-149.4s-3-.4-2.4 2.3c1-2 2.3-2.2 2.4-2.3zm-.3-6.4c-1.7 0-3.8-.2-3 2.5 1-2.1 3-2.4 3-2.5zm12.7 36.3s2.6-.2 2 2.5c-1-2-2-2.4-2-2.5z" transform="scale(.86021 .96774)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-ar" viewBox="0 0 640 480">
|
||||||
|
<path fill="#74acdf" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fff" d="M0 160h640v160H0z"/>
|
||||||
|
<g id="ar-c" transform="translate(-64)scale(.96)">
|
||||||
|
<path id="ar-a" fill="#f6b40e" stroke="#85340a" stroke-width="1.1" d="m396.8 251.3 28.5 62s.5 1.2 1.3.9c.8-.4.3-1.6.3-1.6l-23.7-64m-.7 24.2c-.4 9.4 5.4 14.6 4.7 23s3.8 13.2 5 16.5c1 3.3-1.2 5.2-.3 5.7 1 .5 3-2.1 2.4-6.8s-4.2-6-3.4-16.3-4.2-12.7-3-22"/>
|
||||||
|
<use xlink:href="#ar-a" width="100%" height="100%" transform="rotate(22.5 400 250)"/>
|
||||||
|
<use xlink:href="#ar-a" width="100%" height="100%" transform="rotate(45 400 250)"/>
|
||||||
|
<use xlink:href="#ar-a" width="100%" height="100%" transform="rotate(67.5 400 250)"/>
|
||||||
|
<path id="ar-b" fill="#85340a" d="M404.3 274.4c.5 9 5.6 13 4.6 21.3 2.2-6.5-3.1-11.6-2.8-21.2m-7.7-23.8 19.5 42.6-16.3-43.9"/>
|
||||||
|
<use xlink:href="#ar-b" width="100%" height="100%" transform="rotate(22.5 400 250)"/>
|
||||||
|
<use xlink:href="#ar-b" width="100%" height="100%" transform="rotate(45 400 250)"/>
|
||||||
|
<use xlink:href="#ar-b" width="100%" height="100%" transform="rotate(67.5 400 250)"/>
|
||||||
|
</g>
|
||||||
|
<use xlink:href="#ar-c" width="100%" height="100%" transform="rotate(90 320 240)"/>
|
||||||
|
<use xlink:href="#ar-c" width="100%" height="100%" transform="rotate(180 320 240)"/>
|
||||||
|
<use xlink:href="#ar-c" width="100%" height="100%" transform="rotate(-90 320 240)"/>
|
||||||
|
<circle cx="320" cy="240" r="26.7" fill="#f6b40e" stroke="#85340a" stroke-width="1.4"/>
|
||||||
|
<path id="ar-h" fill="#843511" stroke-width="1" d="M329 234.3c-1.7 0-3.5.8-4.5 2.4 2 1.9 6.6 2 9.7-.2a7 7 0 0 0-5.1-2.2zm0 .4c1.8 0 3.5.8 3.7 1.6-2 2.3-5.3 2-7.4.4q1.6-2 3.8-2z"/>
|
||||||
|
<use xlink:href="#ar-d" width="100%" height="100%" transform="matrix(-1 0 0 1 640.2 0)"/>
|
||||||
|
<use xlink:href="#ar-e" width="100%" height="100%" transform="matrix(-1 0 0 1 640.2 0)"/>
|
||||||
|
<use xlink:href="#ar-f" width="100%" height="100%" transform="translate(18.1)"/>
|
||||||
|
<use xlink:href="#ar-g" width="100%" height="100%" transform="matrix(-1 0 0 1 640.2 0)"/>
|
||||||
|
<path fill="#85340a" d="M316 243.7a1.8 1.8 0 1 0 1.8 2.9 4 4 0 0 0 2.2.6h.2q1 0 2.3-.6.5.7 1.5.7a1.8 1.8 0 0 0 .3-3.6q.8.3.8 1.2a1.2 1.2 0 0 1-2.4 0 3 3 0 0 1-2.6 1.7 3 3 0 0 1-2.5-1.7q-.1 1.1-1.3 1.2-1-.1-1.2-1.2c-.2-1.1.3-1 .8-1.2zm2 5.4c-2.1 0-3 2-4.8 3.1 1-.4 1.8-1.2 3.3-2s2.6.2 3.5.2 2-1 3.5-.2l3.3 2c-1.9-1.2-2.7-3-4.8-3q-.7 0-2 .6z"/>
|
||||||
|
<path fill="#85340a" d="M317.2 251.6q-1.1 0-3.4.6c3.7-.8 4.5.5 6.2.5 1.6 0 2.5-1.3 6.1-.5-4-1.2-4.9-.4-6.1-.4-.8 0-1.4-.3-2.8-.2"/>
|
||||||
|
<path fill="#85340a" d="M314 252.2h-.8c4.3.5 2.3 3 6.8 3s2.5-2.5 6.8-3c-4.5-.4-3.1 2.3-6.8 2.3-3.5 0-2.4-2.3-6-2.3"/>
|
||||||
|
<path fill="#85340a" d="M323.7 258.9a3.7 3.7 0 0 0-7.4 0 3.8 3.8 0 0 1 7.4 0"/>
|
||||||
|
<path id="ar-e" fill="#85340a" stroke-width="1" d="M303.4 234.3c4.7-4.1 10.7-4.8 14-1.7a8 8 0 0 1 1.5 3.4q.6 3.6-2.1 7.5l.8.4q2.4-4.7 1.6-9.4l-.6-2.3c-4.5-3.7-10.7-4-15.2 2z"/>
|
||||||
|
<path id="ar-d" fill="#85340a" stroke-width="1" d="M310.8 233c2.7 0 3.3.6 4.5 1.7 1.2 1 1.9.8 2 1 .3.2 0 .8-.3.6q-.7-.2-2.5-1.6c-1.8-1.4-2.5-1-3.7-1-3.7 0-5.7 3-6.1 2.8-.5-.2 2-3.5 6.1-3.5"/>
|
||||||
|
<use xlink:href="#ar-h" width="100%" height="100%" transform="translate(-18.4)"/>
|
||||||
|
<circle id="ar-f" cx="310.9" cy="236.3" r="1.8" fill="#85340a" stroke-width="1"/>
|
||||||
|
<path id="ar-g" fill="#85340a" stroke-width="1" d="M305.9 237.5c3.5 2.7 7 2.5 9 1.3 2-1.3 2-1.7 1.6-1.7s-.8.4-2.4 1.3c-1.7.8-4.1.8-8.2-.9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,109 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" version="1.0" id="flag-icons-arab" viewBox="0 0 640 480">
|
||||||
|
<path fill="#006233" d="M0 0v480h640V0Z" class="arab-fil0 arab-str0"/>
|
||||||
|
<g fill="#fff" fill-rule="evenodd" stroke="#fff">
|
||||||
|
<path stroke-width=".4" d="M1071.9 2779.7c-25.9 38.9-7.2 64.2 19.5 66 17.6 1.3 54.2-24.9 54.1-55.7l-10-5.6c5.6 15.8-.2 20.8-12.1 31.6-23.5 21.3-71.5 22.8-51.5-36.3z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path d="M1277.2 2881.7c145.8 4.1 192.2-137 102.2-257.8l-8.9 13.3c5.8 56.3 14.2 111.8 15 169.5-17.6 20.7-43.2 13-48.3-10 .3-31.2-9.9-57.6-22.8-82.8l-7.2 13.3c8.4 20.7 17.5 44 19.4 69.5-41.6 49.9-87.6 60-70.5-5.6-32.9 57.5 16.9 98 73.3 9.5 12.1 60.4 58.9 22.9 61.7 9.9 5.1-39.6 2.5-103.4-7.8-153.8 40.6 70.3 42 121 20.4 154.9-24 37.7-76.2 55.3-126.5 70.1z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path d="M1359.9 2722.2c-31.2 2.3-47.2-4.1-30.3-27.2 16.7-22.6 32.3-4.6 36.5 25.6 3.9 28.3-54.8 64.4-75.1 64.4-30.7 0-44.9-39.5-16.6-75-36.4 103.6 78.6 43.5 85.5 12.2zm-21.6-24c-3.8-.2-6.6 6.5-4.7 7.8 5.5 3.8 14.2 1.5 15.1-.4 1.9-4.2-5.1-7.2-10.4-7.4z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path d="M1190.5 2771.1c-30 59-.1 83.4 38.4 76.6 22.4-4.1 50.8-20 67.2-41.7.3-47.8-.4-95.2-4.6-141.5 15-17.9-1.3-17.8-7-37-2.6 11.2-8.9 23.3-2.8 32q6.6 70 6.6 142.2c-30.2 24.3-52.9 33.3-69.1 33.1-33.5-.3-40.7-28.5-28.7-63.7z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path d="M1251.8 2786.7c-.5-44.5-1.2-95-5.2-126.1 15.6-17.3-.8-17.7-5.9-37.1-3 11-9.6 23-3.8 31.9 2.6 47.6 5.1 95.2 5.6 142.8 3.6-2.3 7.7-3.2 9.3-11.5z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path stroke-width=".4" d="M1135.4 2784.6c-3.8-4.8-6.5-10.2-9.6-14.9-.5-6.7 4-12.9 4.6-16.3 5.1 7.9 8.1 13.9 12.2 17.8m5.4 3.1c7.5 3 16.7 3 25.2 3.2 32.8.6 67.3-4.8 63.6 39.6a66 66 0 0 1-65.2 61.9c-41.7-.4-77.3-46.4-13-131.1 6.2-1 14.3.7 21 1.3 11.5.9 23.3-.2 36.8-11-1.6-27.9-1.6-54.3-5-79.5-5.8-8.9.8-20.8 3.8-31.9 5.1 19.4 21.4 19.8 5.9 37.2 3.7 28 4.1 56.5 4.1 73.5-7.8 11.9-13.9 24.5-36.7 29.3-23.3-3.4-33.8-36-58.1-25.2 6.7-29.4 68.4-36.1 74.6-12.9-4.1 24.2-61.7 14.5-77 92.7-4.7 24.1 20.7 46.3 46.8 44.5 25.5-1.7 52.7-19.4 55.4-49.2 2.1-24.9-33-22-47.7-21.7-21.4.5-34.9-2.8-43-7.5m21.9-53.9c3.8-3.6 17.1-6.1 21.9-.3q-5.5 3.5-10 8.1c-5-2.6-8.3-5.2-11.9-7.8z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path d="M1194 2650.9a49 49 0 0 1 5.3 21c-2.2 10.4-11.1 20.1-20.3 20.4-5.7.2-12.1-1.4-16.6-10.3-.5-1.1-2.9-3.7-5.2-2.5-10.1 16.6-17.6 23.6-26.7 23.5-18.2-.3-12.8-16.5-29.6-21.5-7-.2-18.5 6.9-24.4 20.8-22.4 63.5-42.8-.2-34.1-29.8 1.3 28.3 8.1 45.1 15.1 44.6 5.1-.5 9.6-12.3 16.1-24.7 5-9.5 17-26.6 29.7-26.6 11.6.3 4.3 21.6 27.5 21.3 11.2-.2 21.5-8.8 31.9-26 2.3-.4 2.9 3.7 3.4 5.1 1.6 5.9 11.8 22.1 25.6 7.3-.7-3.2-.4-8.5-3.9-9.6z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path stroke-width=".4" d="M1266.9 2598.3c-12.3 6.1-21.3.5-26.4-4.9 8.9-1.8 15.8-5 17.8-12-4-9-13.5-12.9-26.9-13-17.9.5-27.1 7.7-28.2 17.6 8.3.3 15.8-2 19 6-14.7 7.2-32 9.8-50.8 9.7-30.8 1.6-35.3-12.3-43.4-24.5-.6-.8-3.3-2.1-4.7-1.9-9.5 0-16.5 33.2-27.2 33.1-10.7-1.4-8.3-21.4-11.4-32.8-2.6 17.9 3.3 84.5 36.4 12.2 1-2.4 2.4-1.7 3.3.3 8.9 20.2 27 27.2 46.5 28.2 16.3.9 37.1-6.2 59.4-18.8 5.9 6.5 10.6 13.9 23 15.3 14.5.7 30-9.8 33.5-22.8 1.8-6.7 2.1-19.9-5-20.1-9.9-.3-17.1 23.7-14.8 45.3.2-.3 1.3-5.4 1.3-5.4m-43.8-28.8c6.5-3 12.8-4.4 17.8 2.2a27 27 0 0 0-8.4 4c-2.8-2.2-6.6-3.3-9.4-6.2zm47.8 14.9c1.6-7.1 2.5-12.8 8.3-16.5 1.2 7.5 1.4 11.7-8.3 16.5zm39 11c-1.9-6.1-3.8-11.4-4.4-18-1.4-13.4 10.1-21 20.5-19.9 10.7 1.1 17.8 5.1 28 8.6 8 2.7 18.8 4.8 29.1 7.7 5.8 2.6 0 9.4-1.5 10.3-25.8 10.1-44.1 26.1-60.5 26.8q-14.6.7-26.4-19c-.5-25.4-1.4-55.2-3.9-73.9 3.8-3.8 4.6-6.6 6.4-9.7 2 24.7 2.8 50.7 3.3 76.9 2.1 4.5 4.7 8.3 9.4 10.2zm16.5 2c-13.8 3.9-12.1-7.8-13.4-15-1.5-8.4-.5-17.9 10.2-15.5 13.9 3.7 26.6 8.6 38.9 13.8z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path stroke-width=".4" d="m1314.3 2621.3 1.9 9.3h1.5l-.6-8.7" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m1094.2 2718.5 7-7.2 8.1 6.9-7.5 6.7zm17.8-2.4 7.1-7.2 8.1 6.9-7.5 6.7zm-49.5-74.6 7.1-7.2 8.1 6.9-7.5 6.7zm3.2 21.2 7.1-7.2 8 6.9-7.5 6.7zm128.5 35.5 6.5-5.3 6 6.5-6.8 4.8zm-85.8-135.7 4.6-4.7 5.3 4.5-4.9 4.4zm11.7-1.5 4.6-4.8 5.3 4.6-4.9 4.3zm245.6 53.7-4.4 3.7-4.2-4.3 4.6-3.4z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path stroke-width=".4" d="m1158.7 2747.4-.5 7.9 12.6 1.2 10.1-7.6z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
<path d="m1265.2 2599.8 3.7-.8-.4 10.3-2.3.9z" transform="matrix(.36355 0 0 .3308 -130 -670.9)"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#fff" d="M320 326.3c51.6 0 93.6-38.2 93.6-85.2a82 82 0 0 0-32.6-64.4 70 70 0 0 1 19.2 48c0 40.8-35.9 73.9-80.2 73.9s-80.2-33.1-80.2-74c0-18.3 7.2-35.1 19.2-48a82 82 0 0 0-32.6 64.6c0 46.9 42 85.1 93.6 85.1" class="arab-fil2"/>
|
||||||
|
<g fill="#fff" stroke="#000" stroke-width="8">
|
||||||
|
<path d="M-54 1623c-88 44-198 32-291-28-4-2-6 1-2 12 10 29 18 52-12 95-13 19 2 22 24 20 112-11 222-36 275-57zm-2 52c-35 14-95 31-162 43-27 4-26 21 22 27 49 5 112-30 150-61z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M0 1579c12 0 34-5 56-8 41-7 11 56-56 56v21c68 0 139-74 124-107-21-48-79-7-124-7s-103-41-124 7c-15 33 56 107 124 107v-21c-67 0-97-63-56-56 22 3 44 8 56 8z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M54 1623c88 44 198 32 291-28 4-2 6 1 2 12-10 29-18 52 12 95 13 19-2 22-24 20-112-11-222-36-275-57zm2 52c35 14 94 31 162 43 27 4 26 21-22 27-49 5-112-30-150-61z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M3 1665c2 17 5 54 28 38 31-21 38-37 38-67 0-19-23-47-69-47s-69 28-69 47c0 30 7 46 38 67 23 16 25-21 28-38 1-6 6-4 6 0z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#fff" stroke="#000" stroke-width="8">
|
||||||
|
<path d="M-29 384c-13-74-122-79-139-91-20-13-17 0-10 20 20 52 88 73 119 79 25 4 33 6 30-8z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M4 386c11-76-97-112-110-129-15-18-17-7-10 14 13 45 60 98 88 112 23 12 30 17 32 3z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M93 430c10-91-78-105-101-134-15-18-16-8-11 13 10 46 54 100 81 117 21 13 30 18 31 4z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M66 410c-91-59-155-26-181-29-25-3-33 13 10 37 53 29 127 25 156 14 30-12 21-18 15-22zm137 40c-28-98-93-82-112-94s-21-9-17 13c8 39 75 82 108 95 12 4 27 10 21-14z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M190 467c-78-63-139-16-163-23-18-5-10 7-3 12 50 35 112 54 160 32 19-8 20-10 6-21zm169 64c1-62-127-88-154-126-16-23-30-11-22 26 12 48 100 101 148 111 29 6 28-4 28-11z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M355 542c-81-73-149-49-174-56-25-6-35 9 4 39 48 36 122 43 153 36s23-14 17-19zm145 107c-23-106-96-128-114-148-17-20-35-14-20 34 18 57 77 107 108 119 30 13 28 3 26-5z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M499 663c-59-95-136-92-160-105-23-14-39-2-8 39 36 50 110 78 144 80s28-7 24-14z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M575 776c34-108-44-148-52-166-9-18-18-18-23 1-22 77 49 152 60 167 11 14 13 7 15-2z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M559 806c-27-121-98-114-114-131-17-17-19-5-16 17 8 59 79 99 111 119 10 6 22 13 19-5zm68 142c49-114-9-191-27-208-18-16-29-23-23 0 8 35-20 125 23 191 14 22 16 43 27 17z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M601 971c11-70-29-134-72-159-25-15-26-11-26 10 2 65 63 119 81 149 17 28 16 7 17 0z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M590 1153c-36-132 39-208 62-223 22-16 36-22 26 3-15 37 1 140-56 205-18 22-25 45-32 15z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M598 1124c30-115-35-180-55-193-19-13-31-18-22 3 12 32-1 122 49 178 16 19 22 38 28 12z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M561 1070c-54 58-55 143-31 193 15 29 17 27 31 6 38-61 15-149 17-188 1-37-11-17-17-11z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M650 1162c0 80-49 145-101 165-30 11-30 8-26-16 14-90 83-123 108-152 24-28 19-5 19 3z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M464 1400c88-80 41-136 45-188 2-28-9-21-19-11-56 55-59 153-47 191 5 17 13 15 21 8z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M582 1348c-29 88-106 142-171 145-38 2-37-1-24-27 49-94 136-105 175-129 36-22 23 2 20 11z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M343 1513c114-57 91-152 112-176 15-17-3-15-12-9-67 39-121 101-122 167 0 25 2 28 22 18z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M187 1619c144 23 211-86 253-96 22-5 6-14-5-15-96-11-218 34-255 84-15 20-15 24 7 27z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M333 1448c-29 95-137 173-218 179-38 3-38-1-24-26 65-118 178-138 218-168 34-26 27 6 24 15zM29 384c13-74 122-79 139-91 20-13 17 0 10 20-20 52-88 73-119 79-25 4-33 6-30-8z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-4 386c-11-76 97-112 110-129 15-18 17-7 10 14-13 45-60 98-88 112-23 12-30 17-32 3z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-93 430c-10-91 78-105 101-134 15-18 16-8 11 13-10 46-54 100-81 117-21 13-30 18-31 4z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-66 410c91-59 155-26 181-29 25-3 33 13-10 37-53 29-127 25-156 14-30-12-21-18-15-22zm-137 40c28-98 93-82 112-94s21-9 17 13c-8 39-75 82-108 95-12 4-27 10-21-14z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-190 467c78-63 139-16 163-23 18-5 10 7 3 12-50 35-112 54-160 32-19-8-20-10-6-21zm-169 64c-1-62 127-88 154-126 16-23 30-11 22 26-12 48-100 101-148 111-29 6-28-4-28-11z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-355 542c81-73 149-49 174-56 25-6 35 9-4 39-48 36-122 43-153 36s-23-14-17-19zm-145 107c23-106 96-128 114-148 17-20 35-14 20 34-18 57-77 107-108 119-30 13-28 3-26-5z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-499 663c59-95 136-92 160-105 23-14 39-2 8 39-36 50-110 78-144 80s-28-7-24-14z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-575 776c-34-108 44-148 52-166 9-18 18-18 23 1 22 77-49 152-60 167-11 14-13 7-15-2z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-559 806c27-121 98-114 114-131 17-17 19-5 16 17-8 59-79 99-111 119-10 6-22 13-19-5zm-68 142c-49-114 9-191 27-208 18-16 29-23 23 0-8 35 20 125-23 191-14 22-16 43-27 17z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-601 971c-11-70 29-134 72-159 25-15 26-11 26 10-2 65-63 119-81 149-17 28-16 7-17 0z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-590 1153c36-132-39-208-62-223-22-16-36-22-26 3 15 37-1 140 56 205 18 22 24 45 32 15z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-598 1124c-30-115 35-180 55-193 19-13 31-18 22 3-12 32 1 122-49 178-16 19-22 38-28 12z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-561 1070c54 58 55 143 31 193-15 29-17 27-31 6-38-61-15-149-17-188-1-37 11-17 17-11z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-650 1162c0 80 49 145 101 165 30 11 30 8 26-16-14-90-83-123-108-152-24-28-19-5-19 3z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-464 1400c-88-80-41-136-45-188-2-28 9-21 19-11 56 55 59 153 47 191-5 17-13 15-21 8z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-582 1348c29 88 106 142 171 145 38 2 37-1 24-27-49-94-136-105-175-129-36-22-23 2-20 11z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-343 1513c-114-57-91-152-112-176-15-17 3-15 12-9 67 39 121 101 122 167 0 25-2 28-22 18z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-187 1619c-144 23-211-86-253-96-22-5-6-14 5-15 96-11 218 34 255 84 15 20 15 24-7 27z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
<path d="M-333 1448c29 95 137 173 218 179 38 3 38-1 24-26-65-118-178-138-218-168-34-26-27 6-24 15z" class="arab-fil2 arab-str2" transform="matrix(.23458 0 0 .21345 320 27.3)"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#006233" d="M359.6 128.9c-4.4-3-20.8-1.3-23.9-3.3 5.9 4.5 19 1.3 24 3.3zm39.7 7.6c-3.5-5.7-24.4-9.6-27.5-14.7 5.5 9.8 21.6 8.5 27.5 14.7m-3 6.6c-7.8-6.8-25.8-4-31.3-8 12.7 10.4 19.7 2.3 31.2 8zM351 112.8c4.9 2.4 11 4.7 14 10.3-3.5-4.3-9.8-6-15-9.6q.5-.1 1-.7m77 44c-3.1-6.4-14-13.4-14.9-15.8 3 8.3 12 10.3 14.8 15.8zm2.7 11.3c-9.4-13.4-24.1-12-30-17 4.5 4.9 21.4 8 30 17m21.8 20.7c.7-14.3-11-19.6-11.4-27.7-.3 9.6 12 22.6 11.4 27.7m-5.8 7.7c-2.4-12.4-18.3-13.2-21.1-20.5 0 6.8 18.7 13.9 21 20.5zm13.1-7c8.5 9.4 2.6 23.7 6.1 34.1-4.2-7.7-2.1-26.9-6-34.1zm-13.8 40c12.6 12.5 7.5 26.3 12.6 32.3-6.3-8.3-5.4-24.5-12.6-32.2zm26.3 1.8c-10.9 10.9-4.3 27.3-10 35 6.4-6.6 5.5-27 10-35m-13.7 0c-1.4-12.6-14.3-19.2-15.4-26-1.5 6.8 12.4 17.5 15.4 26m-6.5 30c2 8.8-5.7 27.6-3.3 33.4-5.2-10 4.4-29 3.3-33.3zm16.6 20.1c-5.1 15.6-15.5 14.6-18.7 24 2.3-9 16-17.1 18.7-24m-33.5 7.3c-6.8 10.5-1.2 22.4-6.8 29.9 8-7.5 3.7-21.4 6.8-29.9m16.4 28.6c-8.2 13.9-25.1 12.6-31.9 22.6 6.8-12.6 27.7-14.7 32-22.6zm-29.8-1.7c-14.5 9.2-10 18.8-21.1 29 13.8-10.2 12.7-21.5 21.1-29m-6.8 37.2c-14-.5-34.2 16.2-46.4 14.9 12.2 2.4 34.7-12.6 46.4-15zm-22.7-15c-1 13-37.6 21.4-41.5 30.1 4.4-11.5 36.6-20 41.5-30zm-82.8-240c-4.7-3.7-10.4-6.7-12-10.3 1.2 4.7 5.8 8 10.5 11.3.5-.2 1-.9 1.5-1.1zm-8 3.7c-7.3-3.2-15.7-3-19.5-7.4 2.4 4.4 10.3 6.1 17.1 8.5q1.2-.7 2.4-1zm-21.1 27.3c4.4-3 20.8-1.2 23.9-3.2-5.9 4.5-19 1.3-24 3.2zm-39.7 7.7c3.5-5.7 24.4-9.6 27.5-14.7-5.4 9.8-21.6 8.5-27.5 14.7m3 6.6c7.8-6.8 25.9-4 31.3-8-12.7 10.4-19.7 2.3-31.2 8zm31.3-20c4.4-8.6 17-9.6 20.4-14.8-5 7.7-15.7 9-20.4 14.8m36-7.5c13-5.5 25.7-.8 31.8-3.4-7.5 3.6-25.4 1.9-31.7 3.4zm-98.9 41.2c3-6.4 13.8-13.5 14.8-15.8-3 8.3-12 10.3-14.8 15.8m-2.8 11.3c9.4-13.4 24.1-12 30-17-4.4 4.9-21.3 8-30 17m-21.8 20.7c-.7-14.3 11-19.6 11.5-27.7.2 9.6-12 22.6-11.5 27.7m5.8 7.7c2.4-12.4 18.3-13.2 21.1-20.5 0 6.8-18.7 13.9-21 20.5zm-13.1-7c-8.4 9.4-2.6 23.6-6 34.1 4.1-7.7 2-26.9 6-34.1m13.8 40c-12.6 12.5-7.5 26.3-12.6 32.3 6.3-8.3 5.4-24.5 12.6-32.2zm-26.2 1.8c10.8 10.9 4.2 27.3 9.8 35-6.3-6.6-5.4-27-9.8-35m13.6 0c1.4-12.6 14.3-19.2 15.4-26 1.5 6.8-12.4 17.5-15.4 26m6.5 30c-2 8.8 5.7 27.6 3.3 33.4 5.2-10-4.4-29-3.3-33.3zm-16.6 20.1c5.2 15.6 15.5 14.6 18.8 24-2.4-9-16-17.1-18.8-24m33.5 7.3c6.8 10.5 1.2 22.4 6.8 29.9-8-7.5-3.7-21.4-6.8-29.9m-16.4 28.6c8.2 13.9 25.1 12.6 32 22.6-6.9-12.6-27.8-14.7-32-22.6m29.8-1.7c14.5 9.2 10.1 18.8 21.1 29-13.8-10.2-12.6-21.5-21.1-29m6.8 37.1c14-.4 34.3 16.3 46.4 15-12.1 2.3-34.7-12.6-46.4-15m22.8-15c.9 13.1 37.5 21.4 41.5 30.2-4.5-11.5-36.6-20-41.6-30.1zM301 116c2.8-11.5 17-13.6 18.8-20.5-.7 7.3-17.4 15.4-18.8 20.5m41.5-28.6c-2 8.8-17.3 13.7-19.4 20.3.7-9 16.4-14 19.4-20.3m-12 20.8c7.3-10.7 22.3-8 27.5-14.1-3.8 7.2-22.3 7.4-27.5 14z" class="arab-fil0"/>
|
||||||
|
<path fill="none" stroke="#f7c608" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M429.8 240c0 55.5-49.3 100.4-110.3 100.4-60.9 0-110.3-44.9-110.3-100.3 0-55.5 49.4-100.4 110.3-100.4 61 0 110.3 45 110.3 100.4z"/>
|
||||||
|
<path fill="#f7c608" d="m298 340.5-.5 1.2q-.5 1.3-2.1 1.2l-8-1.9 2.6-7.7 8 1.7q1.5.4 1 1.8l-.2 1m-19-4.8.4-1.2q.5-1.4 2-1l7.8 2.5-2.5 7.7q-4.2-1-7.9-2.3c-.8-.4-1-2-.7-2.9"/>
|
||||||
|
<path fill="#006233" d="m296.4 339.8-.3.9q-.5 1-1.7.8l-6.6-1.6 1.8-5.6c2.4.7 4.9 1.2 6.6 1.5q1.3.4 1 1.4l-.2.7m-15.8-4 .3-1q.4-.8 1.6-.6 2.8 1 6.5 2l-1.8 5.6-6.5-1.9c-.7-.4-1-1.5-.7-2.1"/>
|
||||||
|
<path fill="#f7c608" d="m267.7 330.8-.7 1q-.9 1.2-2.4.7c-2-1.2-4.7-2.5-7-3.9l4.8-6.8 7.1 3.7q1.2.8.5 2l-.6 1m-16.7-9.6.7-1q1-1.2 2.3-.5 2.8 2 6.7 4.4l-4.9 6.8-6.7-4.2c-.7-.7-.4-2.3 0-3"/>
|
||||||
|
<path fill="#006233" d="m266.5 329.7-.6.8q-.6.8-1.9.3c-1.6-1-3.8-2-5.8-3.2l3.5-4.9c2 1.3 4.3 2.4 5.9 3.1q1 .6.5 1.6l-.5.6m-13.8-7.9.5-.8q.7-.8 1.8-.2l5.6 3.6-3.5 4.9q-3.1-1.7-5.6-3.5c-.6-.5-.5-1.7-.1-2.2"/>
|
||||||
|
<path fill="#f7c608" d="m241.8 313.7-1 .8q-1.3.8-2.6 0c-1.5-1.6-3.7-3.5-5.5-5.5l6.7-5.3c2 2.1 4.2 4 5.7 5.4q1 1-.1 2l-.9.8m-13-13.4 1-.9q1.2-.8 2.3.2a73 73 0 0 0 5 6l-6.7 5.2q-2.9-3-5.2-5.8c-.5-.8.3-2.2 1-2.8"/>
|
||||||
|
<path fill="#006233" d="m240.9 312.4-.8.6q-.8.6-1.9-.2l-4.6-4.6 4.9-3.8 4.7 4.5q.7 1 0 1.7l-.7.5m-10.8-11.2.7-.6q1-.6 1.8.2 1.8 2.3 4.3 5l-4.9 3.7-4.3-4.8c-.4-.6.1-1.7.6-2.1"/>
|
||||||
|
<path fill="#f7c608" d="m222.2 290.7-1.3.5q-1.3.4-2.4-.6l-3.6-6.8 8.1-3.3c1.3 2.5 2.7 5 3.8 6.6q.5 1.3-.8 2l-1 .4m-8.4-16.2 1.2-.6q1.5-.4 2.2.8a71 71 0 0 0 3 7l-8 3.3a60 60 0 0 1-3.3-6.8c-.2-1 1-2.1 1.9-2.5"/>
|
||||||
|
<path fill="#006233" d="m221.7 289.2-.9.3q-1 .4-1.8-.6l-3-5.6 5.8-2.4 3.2 5.5q.4 1.2-.5 1.6l-.8.3m-7-13.5 1-.3q1-.4 1.6.6l2.5 5.8-5.7 2.4-2.7-5.7c-.2-.7.6-1.6 1.2-1.9"/>
|
||||||
|
<path fill="#f7c608" d="m210.5 263.5-1.4.2a2 2 0 0 1-2-1.2l-1.5-7.4 8.8-1.1a64 64 0 0 0 1.7 7.3q0 1.4-1.4 1.7l-1.2.2m-3-17.7 1.4-.2q1.5-.1 1.8 1.2.1 3.3.7 7.5l-8.8 1.1-1-7.4c.2-.9 1.7-1.7 2.6-1.8"/>
|
||||||
|
<path fill="#006233" d="m210.5 262-1 .1q-1 .1-1.5-1l-1.1-6.2 6.3-.8 1.3 6.1q.1 1.1-1 1.4l-.8.1m-2.5-14.7 1-.2q1.2 0 1.3 1.1.2 2.8.7 6.2l-6.3.8q-.6-3.1-.8-6.1c0-.7 1.1-1.4 1.8-1.5"/>
|
||||||
|
<path fill="#f7c608" d="m207.7 234.5-1.4-.2q-1.5-.2-1.6-1.7c.3-2 .5-4.8 1-7.4l8.7 1.2a65 65 0 0 0-.7 7.4q-.3 1.4-1.8 1.3l-1.2-.2m2.6-17.7 1.4.1q1.4.4 1.4 1.7a69 69 0 0 0-1.7 7.4l-8.8-1.2q.6-3.8 1.4-7.4c.4-.8 2.1-1.2 3-1"/>
|
||||||
|
<path fill="#006233" d="M208.2 233h-1q-1-.4-1.1-1.5l.8-6.1 6.3.8-.6 6.2q-.2 1.1-1.4 1h-.8m2.1-14.9 1 .2q1.1.2 1 1.4-.7 2.6-1.3 6l-6.3-.7 1.1-6.2c.3-.7 1.5-1 2.2-1"/>
|
||||||
|
<path fill="#f7c608" d="m214 206-1.3-.6q-1.3-.5-1-2c1-2 2-4.6 3.2-6.9l8 3.4a70 70 0 0 0-3 7q-.6 1.1-2 .7l-1.2-.5m8-16.4 1.3.6q1.3.6.8 2l-3.8 6.6-8.1-3.4q1.7-3.6 3.6-6.7c.6-.7 2.4-.7 3.2-.3"/>
|
||||||
|
<path fill="#006233" d="m215 204.7-1-.4c-.6-.2-.8-1-.6-1.6l2.6-5.7 5.8 2.4-2.5 5.8q-.6 1-1.6.6l-.8-.3m6.7-13.6.9.4q1 .4.5 1.6l-3.2 5.5-5.7-2.4q1.4-3.1 3-5.6c.4-.6 1.7-.7 2.3-.4"/>
|
||||||
|
<path fill="#f7c608" d="m228.9 180.2-1.1-.9q-1-.9-.4-2.2c1.6-1.6 3.4-3.9 5.2-5.8l6.8 5.3a72 72 0 0 0-5 6 2 2 0 0 1-2.4 0l-.9-.6m12.8-13.7 1 .8q1.2 1 .2 2.2l-5.7 5.3-6.8-5.3q2.8-3 5.6-5.5c.8-.5 2.5 0 3.2.5"/>
|
||||||
|
<path fill="#006233" d="m230.2 179.2-.8-.6q-.7-.7-.1-1.7l4.3-4.9 4.8 3.8-4.2 5q-.8.8-1.8.2l-.6-.5m10.6-11.4.8.6q.7.7 0 1.6l-4.8 4.6-4.8-3.8q2.3-2.6 4.6-4.6c.7-.5 2-.2 2.4.2"/>
|
||||||
|
<path fill="#f7c608" d="m251 159.2-.7-1q-.7-1.2.4-2.3c2-1.1 4.4-2.8 6.8-4.2l4.8 6.8a78 78 0 0 0-6.7 4.4 2 2 0 0 1-2.2-.4l-.7-1m16.5-9.8.7 1q.8 1.4-.4 2.1-3.3 1.5-7.2 3.7l-4.8-6.8q3.5-2.2 7-3.9c1-.2 2.4.7 2.9 1.4"/>
|
||||||
|
<path fill="#006233" d="m252.7 158.6-.6-.7q-.4-1 .4-1.7l5.7-3.5 3.4 4.8-5.5 3.7q-1.1.6-1.8-.3l-.5-.6m13.7-8.2.6.8q.4.9-.5 1.5l-6 3.1-3.4-4.8 5.8-3.3c.8-.2 1.9.4 2.3.9"/>
|
||||||
|
<path fill="#f7c608" d="m279 144.9-.5-1.3q-.2-1.3 1-2l7.9-2.3 2.5 7.7a83 83 0 0 0-7.8 2.6q-1.4.3-2-1l-.3-1m18.8-5.4.4 1.3q.3 1.3-1 1.8l-8.1 1.7-2.5-7.7a85 85 0 0 1 8-2c.9 0 2 1.3 2.3 2"/>
|
||||||
|
<path fill="#006233" d="m280.6 144.7-.3-1q-.1-.8 1-1.4l6.5-2 1.8 5.6-6.5 2q-1.2.4-1.6-.6l-.3-.7m15.7-4.4.3.9q.3 1-1 1.4-3 .5-6.6 1.4l-1.8-5.5 6.6-1.6c.8-.1 1.6.8 1.8 1.4"/>
|
||||||
|
<path fill="#f7c608" d="M310 138.2v-1.3q.2-1.3 1.7-1.7l8.2-.2v8.1a84 84 0 0 0-8.2.4q-1.6 0-1.6-1.5v-1m19.7-.2v1.2q-.1 1.4-1.7 1.5l-8.2-.4V135q4.3 0 8.2.2c1 .2 1.7 1.7 1.7 2.6"/>
|
||||||
|
<path fill="#006233" d="M311.8 138.5v-1q0-.9 1.3-1.2l6.9-.1v5.8q-4 0-6.9.3-1.2-.1-1.3-1v-.9m16.3-.1v.9q0 1-1.3 1l-6.8-.2v-5.8l6.8.1c.8.2 1.3 1.2 1.3 1.9"/>
|
||||||
|
<path fill="#f7c608" d="m340 139.6.3-1.2q.5-1.2 2.1-1.2l8 1.8-2.5 7.8-8-1.6q-1.4-.5-1.1-1.9l.3-1m19 4.7-.4 1.2q-.5 1.4-2 1l-7.8-2.4 2.5-7.8q4.1 1 7.8 2.3c.8.4 1 2 .8 2.8"/>
|
||||||
|
<path fill="#006233" d="m341.5 140.3.2-.9q.5-1 1.7-.8l6.6 1.5-1.7 5.6-6.7-1.4q-1.1-.4-1-1.4l.3-.7m15.8 4-.3.8q-.4 1-1.6.7l-6.5-2 1.7-5.6q3.5.9 6.6 1.9c.7.3 1 1.5.7 2"/>
|
||||||
|
<path fill="#f7c608" d="m370.2 149.1.7-1q.9-1.2 2.4-.7c2 1.1 4.7 2.4 7.1 3.8l-4.7 6.9a81 81 0 0 0-7.3-3.6q-1.2-.9-.5-2.1l.7-1m16.8 9.5-.8 1a2 2 0 0 1-2.2.5l-6.7-4.3 4.7-6.9q3.7 2 6.8 4.2c.7.6.4 2.2-.1 3"/>
|
||||||
|
<path fill="#006233" d="m371.5 150.2.5-.8q.6-.7 1.9-.4l5.8 3.2-3.4 5-6-3.1q-1-.6-.4-1.6l.4-.7m14 7.9-.6.8q-.6.8-1.8.2l-5.6-3.6 3.4-4.9 5.7 3.4c.6.6.5 1.7.1 2.3"/>
|
||||||
|
<path fill="#f7c608" d="m396.3 166 1-.9q1.2-.8 2.5 0l5.6 5.5-6.6 5.3a75 75 0 0 0-5.8-5.3q-1-1 .1-2l.9-.8m13.2 13.3-1 .9a2 2 0 0 1-2.4-.2 72 72 0 0 0-5-5.9l6.7-5.3 5.2 5.7c.4.8-.3 2.3-1 2.8"/>
|
||||||
|
<path fill="#006233" d="m397.2 167.3.7-.6q.8-.5 2 .1l4.6 4.6-4.8 3.8-4.8-4.5q-.8-.9 0-1.6l.7-.5m11 11-.8.7q-.9.6-1.8-.2l-4.3-4.9 4.8-3.8 4.4 4.7c.4.7-.1 1.8-.6 2.2"/>
|
||||||
|
<path fill="#f7c608" d="m416.1 188.9 1.3-.6q1.4-.4 2.4.7l3.7 6.6-8.1 3.5q-2-4-4-6.6-.5-1.3.9-2l1-.5m8.6 16.2-1.3.5c-.8.4-1.8 0-2.1-.7a71 71 0 0 0-3.1-7l8-3.4 3.3 6.9c.2.9-1 2-1.8 2.4"/>
|
||||||
|
<path fill="#006233" d="m416.6 190.4.9-.4q1-.4 1.8.6l3 5.5-5.8 2.5-3.2-5.5q-.4-1 .5-1.6l.8-.3m7 13.5-.8.3q-1 .4-1.7-.6-1-2.6-2.6-5.8l5.8-2.5 2.8 5.7c.1.8-.7 1.7-1.3 2"/>
|
||||||
|
<path fill="#f7c608" d="m428 215.9 1.4-.2a2 2 0 0 1 2.1 1.2l1.5 7.3-8.8 1.3a65 65 0 0 0-1.7-7.3q-.1-1.4 1.4-1.7l1.1-.2m3.2 17.7-1.4.2q-1.5.1-1.8-1.3l-.8-7.4 8.8-1.3 1 7.5c0 .9-1.6 1.7-2.5 1.8"/>
|
||||||
|
<path fill="#006233" d="m428 217.4 1-.1q1.1 0 1.5 1l1.2 6.1-6.3 1-1.3-6.2q-.2-1.2 1-1.3l.8-.2m2.6 14.7-1 .2q-1.1 0-1.4-1l-.7-6.3 6.3-.9q.7 3.3.9 6.2c0 .7-1.1 1.4-1.8 1.5"/>
|
||||||
|
<path fill="#f7c608" d="m431.1 244.9 1.4.1q1.5.3 1.7 1.8l-.9 7.4-8.8-1.1c.4-2.7.6-5.5.6-7.5.1-.8 1-1.4 1.9-1.2l1.1.1m-2.4 17.8-1.4-.2q-1.5-.2-1.4-1.7 1-3.1 1.6-7.3l8.8 1q-.6 4-1.3 7.4c-.4.9-2.1 1.3-3 1.2"/>
|
||||||
|
<path fill="#006233" d="M430.6 246.4h1q1 .4 1.2 1.5l-.8 6.2-6.3-.8.6-6.2q.1-1.1 1.3-1.1h.9m-2 14.9-1-.1q-1.1-.2-1-1.4.7-2.7 1.2-6.1l6.3.8-1 6c-.3.8-1.6 1.2-2.2 1"/>
|
||||||
|
<path fill="#f7c608" d="m425.1 273.5 1.3.5q1.3.7 1 2l-3 7-8.2-3.3a66 66 0 0 0 3-7q.6-1.2 2-.8l1.2.4m-7.9 16.5-1.2-.5q-1.4-.7-.9-2 1.9-2.8 3.8-6.6l8.1 3.3-3.5 6.7c-.6.7-2.4.7-3.3.3"/>
|
||||||
|
<path fill="#006233" d="m424.2 274.8 1 .3q.7.5.6 1.7l-2.6 5.7-5.9-2.3 2.5-5.8q.6-1 1.6-.8l.8.4m-6.5 13.6-1-.3q-1-.6-.4-1.6l3-5.5 5.9 2.3-3 5.6c-.5.6-1.8.7-2.4.4"/>
|
||||||
|
<path fill="#f7c608" d="m410.5 299.4 1.1.8q1 1 .4 2.3c-1.6 1.6-3.4 3.8-5.2 5.8L400 303c2-2 3.8-4.3 5-6q1-.9 2.3-.1l.9.7m-12.6 13.8-1-.8q-1.2-1-.3-2.1 2.6-2.2 5.7-5.5l6.8 5.3-5.5 5.6c-.8.5-2.5 0-3.2-.6"/>
|
||||||
|
<path fill="#006233" d="m409.2 300.4.8.6q.7.6.1 1.7l-4.3 4.8-4.9-3.7q2.6-2.8 4.2-5 .9-.8 1.8-.2l.6.5m-10.4 11.5-.8-.6q-.8-.7 0-1.7l4.6-4.5 5 3.7q-2.4 2.6-4.7 4.7c-.6.4-1.8.1-2.4-.3"/>
|
||||||
|
<path fill="#f7c608" d="m388.5 320.5.7 1q.7 1.3-.3 2.3l-6.7 4.3-5-6.8a78 78 0 0 0 6.7-4.4 2 2 0 0 1 2.2.4l.7.9m-16.4 10-.7-1q-.8-1.3.4-2.2l7.2-3.7 4.8 6.8-7 4c-.9.2-2.3-.7-2.9-1.4"/>
|
||||||
|
<path fill="#006233" d="m386.9 321.1.5.8q.5.8-.4 1.7l-5.6 3.5-3.5-4.8 5.6-3.7q1-.6 1.7.2l.5.7m-13.6 8.3-.6-.8q-.4-.9.5-1.6l6-3.1 3.4 4.8q-3 1.9-5.8 3.3c-.7.3-1.9-.3-2.2-.8"/>
|
||||||
|
<path fill="#f7c608" d="m360.8 335.1.4 1.2q.3 1.4-1 2l-7.8 2.5-2.6-7.8a75 75 0 0 0 7.7-2.6q1.5-.2 2 1l.4 1m-18.8 5.5-.4-1.3q-.3-1.4 1-1.8 3.6-.6 8-1.8l2.7 7.8-8 2c-1 0-2-1.3-2.3-2"/>
|
||||||
|
<path fill="#006233" d="m359 335.3.4.9q.2 1-1 1.5l-6.4 2-1.9-5.6 6.4-2q1.3-.4 1.7.6l.2.7m-15.6 4.5-.3-.9q-.2-1 1-1.4l6.6-1.5 1.9 5.6-6.6 1.6c-.8 0-1.7-.8-2-1.4"/>
|
||||||
|
<path fill="#f7c608" d="M329.7 342v1.3q-.1 1.4-1.6 1.7c-2.4 0-5.4.3-8.2.3l-.1-8.1a82 82 0 0 0 8.2-.5q1.6 0 1.6 1.5v1m-19.6.4v-1.2q0-1.4 1.6-1.5l8.2.3v8.1l-8.2-.1c-.9-.2-1.6-1.7-1.6-2.6"/>
|
||||||
|
<path fill="#006233" d="M328 341.8v.9q-.1.9-1.4 1.2l-6.8.2v-5.7q4-.1 6.8-.4 1.2 0 1.4 1v.8m-16.4.3v-1q0-.9 1.3-1 3 .3 6.9.2v5.8H313c-.8-.2-1.4-1.3-1.4-1.9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,72 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-as" viewBox="0 0 640 480">
|
||||||
|
<path fill="#006" d="M0 0h640v480H0Z"/>
|
||||||
|
<path fill="#bd1021" d="m-.6 240 640-240v480Z"/>
|
||||||
|
<path fill="#fff" d="m59.7 240 580-214.3v428.6"/>
|
||||||
|
<path d="M474 270.4c5.1.3 5 5.4 5 5.4l18 .4c2.3-6.3 4.8-5.6 9.2-2.4a33 33 0 0 0 8.7 4.2c1.7-9 14.5-7.2 14.5-7.2 5.6-13 6-12.9 2.7-14.5a11 11 0 0 1-4.6-4.5c-3-3.7-4.6-9.1-5-12.4s-4.2 1.6-5 .6c-.6-1-6.3-.4-6.3-.4 1.4 1.5-3.4.6-3.4.6.5.4 0 1.7 0 1.7-.4-.6-4.1-1.2-4.1-1.2l-1.1 1.6c-2-.8-6-.7-6-.7a20 20 0 0 0-10.9 2.8c-1.6.9-7.4 3.8-12.3 8.5-4.7 4.6-7.4 4-7.4 4-1.4 5.2-12.8 11.5-12.8 11.5-1.8 1.6-7.6 2.4-10.5 0s0-6.9 0-6.9c1.2-2 2.2-1.9 2.3-9 .1-4.7 5-8.5 10-14 6.3-6.8 15-18 15-18 0 3.4 1.8 4 1.8 4 1.7-3.5 4.2-6.3 4.2-6.3q.3.4.5.4l3-3.6c-.5-.3-6 0-11 4.4s-8.4 3-8.4 3c-3.5-1.2-3.8-4-3.8-4-2.5-10.9 7.4-18.7 7.4-18.7-13.4-3.2-3.7-20.3 13-27.5s16.4-10.5 16.4-10.5a13 13 0 0 1 1.8 3c.1 0 1.4-1.9 11-6.1 9.6-4.3 14.2-8 14.2-8 1.2 2.4 1 4 1 4 26.3-9.1 52-30.2 52-30.2.8 1.7.5 4.4.5 4.4 4.2-4 19.7-13.2 19.7-13.2a9 9 0 0 1-4.6 8.2l.8 2.3a360 360 0 0 0 14.4-9.5c4.3 3.7.4 9.8.4 9.8 1.6-.3 2.6-1.6 2.6-1.6 1.2 6.4-5.9 12-5.9 12 1.3 0 3.3-1.3 3.3-1.3-1.3 7-14.4 14.6-14.4 14.6 1.9 1.8 0 4-1.6 5-1.5 1-4.3 3.3-3.4 4.2s6.7-3.2 6.7-3.2c1 2.9-6.5 8.6-6.5 8.6 5.2.7 19.6-5.9 19.6-5.9-1.1 5.6-6.6 10-13.3 12.5s-6.4 3-6.4 3c1.2.8 10.5-1.8 10.5-1.8-2.8 6.2-12.5 10.5-12.5 10.5 2.7 2.3 6.3-.4 10-2.9a58 58 0 0 1 14-6.4c5.3-1.9 9.2-.5 9.2-.5a12 12 0 0 1 8.4.6c8.7.7 9.6 3.9 9.6 3.9 1 .2 1.7.6 4 2.3 2.1 1.6 2 6.6 2 9.2-.2 2.4-.9 2.4-1.3 3q-.5 1.2-.5 2.5c0 1-2.2 6.9-15.7 6.9h-20.3c-1.2 0-2.5.7-2.5.7-5.7 2.8-2.7-2-9.4 3.6s-10.2 4.6-10.2 4.6A90 90 0 0 1 568 221c-4 2.6-3.3 2.3.3 3.8s8.8 0 8.8 0c-3.4 2.3-1 3.4-1 3.4 4.4-2.7 7.2-1.7 7.2-1.7 1.4 3.9-3.8 10-3.8 10 2 .3 5.8 0 5.8 0-1 2.7-4.6 5.6-7.4 6.4-2.7 1-2.3 1.3-1.4 3 .7 1.6.1 3.3.1 3.3-4.8-3.3-5-.4-5-.4-.5 4-.4 9.6-.4 9.6-3.4-1.7-3.5.5-3.5.5-1 3.6-5.1 7.7-5.1 7.7-.2-2.2-2.2-2.8-2.2-2.8-2.2 4.2-6.1 6.7-6.1 6.7-.5 3.5.5 8.6.5 8.6-2.6-.6-3.5-.6-4 0-.3.7.6 1 .6 1l33.4.8c.5 0 2.5.3 2.5 3.8 0 3.7-3 3.9-3 3.9l-36.4-.9s.1 1-1.8 2-1.2-1.1-1.7 3.4-7.8-.4-7.8-.4c-1.2 1.8-4 4-4 4-1.7-5-3.4-6.4-6-2.2s4.8 3.6 4.8 3.6 42.8-6.3 45.1-6.5 4.9-.1 6 3.1-5.3 3.8-5.3 3.8l-44 4.8c-.9 2.6-4.5 2.4-4.5 2.4.3 2.5-2.3 4-3.6 5-1.4.8-5.6.5-5.6.5-5 3.4-7.6.7-7.6.7-3.3 1.4-5.4.8-8.1-.4-2.8-1.2-2.5-4.5-2.5-4.5l-27.8 3a7 7 0 0 0-2.2 1.2c1 1.3-2 4.3-2 4.3.9.5 2.5 2.1 2.7 5.5.2 3.7-4.5 4.3-2.2 7 2.3 2.5 6.7.3 11.5-2s9.5-2 11.5-2 7.8 1.6 11.4 2.8 4.8.4 5-1.4 1.9-2.3 1.9-2.3c-.5 1.8.5 2.6.5 2.6a11 11 0 0 0 3.7-1.3c-.2 1.4-2 2.2-2 2.2-3.4 2.3 1.4 1.5 1.4 1.5a44 44 0 0 1 15.4-1.5 123 123 0 0 1 14.3 5.2c.4-1.2.1-4 .1-4 3 .8 4.2 2.5 4.2 2.5 1.2-1.2.4-3.4.4-3.4 9.7 5.5-2 8-5.1 9s-3 2.3-3 2.3a28 28 0 0 1 6.4-1.3c2.2-.2 1.4 0 6.5-1 5.2-1 7.8 1.2 7.8 1.2-4.3.2-5.5 1.5-5.5 1.5 2.6 1.7 0 3.4 0 3.4-3.8-5-7.2.1-7.2.1a15 15 0 0 1 6.4 1.4l5.4 2.7c3.6 1.6 2.9.6 5.6 1.6 2.8 1 1.7 3.7 1.7 3.7a7 7 0 0 0-3.7-3c-.2 3-3.1 3.5-3.1 3.5 3.6-4-4.1-5.8-7.8-5.7-3.6 0-6.3 2.4-6.3 2.4 7.3 6.9 12.3 4.6 12.3 4.6-.9 2.5-6.9 1.5-6.9 1.5 2.8 2.2 2.5 3.6 2.5 3.6-1.5-1.4-4-.7-9.2-4-5.2-3.5-9.9-2.3-9.9-2.3 5.2 5.3-1.8 8.6-1.8 8.6-2.6 1.6 1 3.5 1 3.5-3.2.6-3.6-2.6-3.6-2.6-1.7-.4-4.2 1.6-4.2 1.6.2-3.2 4.6-1.6 4.6-5 .2-3.5-4-6.2-16.3-4.5s-16-2.2-16-2.2c-1 0-1.2 1-1.2 1 2 2 2.9 2.8 2.6 4.2-.4 1.3.6 1.8.6 1.8-2.3-.2-2.4-2.8-2.4-2.8 0 1.1-.5 1.2-1.3 2.3s0 2.7 0 2.7c-1-.8-2.7-1.8-1-4.3 1.2-1.8-2.7-4.2-2.7-4.2-1.5-1.5-5.6 0-5.6 0a15 15 0 0 1-13.3-3.7c-1 0-2.9-.6-2.9-.6-8.9 4-16.7-4.6-16.7-4.6-6.7 1.3-9.8-2-11.8-5.2a12 12 0 0 0-5.2-5c-2.6-1.6-5.2-6.2-2.6-8.7 2-2.1 1.5-2.6 1.5-2.6-3.5-5.9 6.1-7.7 6.3-9.2.3-2 2.3-3.3 4.5-3.4s2.2 0 3.7-1.4c1.3-1.5 4 .3 4 .3.7-.4 5.5-4.1 9.7-2.2 4.3 1.9 7.9.6 7.9.6 3-.7 28-4 28-4 1.5-2.5 2.7-5.4 9.6-7s12-6 12-6c-1.2-1.2-3.2-1.2-4.2-1.3-1.1 0-3.2-2-3.2-2-1.3.6-2 .3-11 5.8-8.1 5-8.3-4.8-8.3-4.8H479c-.3 3.7-3 5.2-3 5.2l-6.5.3c-3.6-1.8-3.6-8.2-3.6-8.2-19.4.3-30.1 7.2-30.1 7.2-22-11.2-39.2-13.8-39.2-13.8a122 122 0 0 0 40.8-10.2 63 63 0 0 0 28.5 9c.5-5.4 4.1-6.7 4.1-6.7z"/>
|
||||||
|
<path fill="#ffc221" d="M442.3 314.6c-5.5 3.2-4.5 5-4 6s.5 2-1 3.6c-1.5 1.5-1.4 2-1.4 2 .3 5.4 4 6.6 5.7 8 1.4 1 3.6 4.5 3.6 4.5 2.9 4.1 5.9 4.2 8.1 4.2 2.3 0 2-.3 1-1.3l-3.4-2.7a18 18 0 0 1 5.9 4.1c5.6 6.2 10.8 5.4 13.1 5.2s2-1.7 2-1.7l-2.4-.4c-8.5-.8-11-6.4-11-6.4a24 24 0 0 0 15.6 6c2.4-.1 2.3.6 1.7.8l-2.4-.2c-1.1 0-1.1.3-.9.8q.6.5 2.7.4c1.4 0 .3.1 3.8 2.8 3.6 2.8 12.3.5 12.3.5-5.7-1.3-6.4-4-6.4-4-7.7 1-10.8-3.6-10.8-3.6a33 33 0 0 0-5.6-3.5 9 9 0 0 1-5-5.8c1.3 1.8 3.7 3.8 6.7 4.6s3.8 1.2 3.8 1.2a4 4 0 0 1-2.3-.2c-3-1-1.3.3-1.3.3 3.4 2.7 4.3 2.5 4.3 2.5 8.6.9 4.3-2.6 4.3-2.6 6.2 1.5 7.2-.8 7.2-.8 1.3 2.7 6 1.7 6 1.7-6.2 3-1.5 2.1-1.5 2.1 6.3-1.1 7.6.5 7.6.5 1.6 1.5 3.4 1.4 3.4 1.4s1.2 0 3.5.4c2.4.5 6.2 2.5 9.6 2.2 3.5-.5 4 .6 4 .6-.6-.3-2.2-.5-4.8.7-2.7 1.3-7.4 1.6-14.2 0s-7.4-1.3-7.4-1.3a9 9 0 0 1 3.4 4c.3 1.2 1.5 1.2 1.5 1.2.5-1.5 2.5-2.1 2.5-2.1a27 27 0 0 0 5 2.8c.4-.7 0-1.3 0-1.3 2.6 2.5 5.6 1.7 5.6 1.7.8-.5.6-2 .6-2 1 0 1.2.6 2 1.2.7.4 3 .1 3 .1-.8-.4-1.5-1.7-1.5-1.7 3.5-2.3 11-1.3 11-1.3 5.3 1 4.7 4.5 4.7 4.5a10 10 0 0 1 2.5 2.1c.5-1.2 0-2.5 0-2.5 2.6 1.2 3 4 3 4 3-3.2-2.7-6.8-2.7-6.8 2.7-.4 5.7-.2 7.5 0a14 14 0 0 1 6.6 3.1c2.1 1.7 5.9 2.5 5.9 2.5-.1-.7-2.2-2-2.7-2.2s-.6-.9-.6-.9c1.9.4 3.1.2 3.1.2-6.4-4-8.1-5.9-8.1-5.9 2.4.3 3.8-1.2 3.8-1.2-5.1 0-5.4-1.2-5.4-1.2.7.1 3.1.7 6.2.1s7.2 0 7.2 0c-2.2-3.6-10.7-3-13.5-2.8s-3.8-.2-3.8-.2c.4-.2.9-.6 3-.7 2.2 0 4.3.2 6.8-1.6 2.3-1.6 5.7-1 5.7-1-.8-1.6-4.7-2.2-8 0-3.5 2.1-6.5 1.5-6.5 1.5 5.3-.8 6.9-2.7 6.9-2.7-1.6-.4-2.5.1-5.8.8-3.2.6-4-.5-4-.5 3.5-2.1 6-3 6-3-3-.6-5.8-2-5.8-2-3.2 3-5.6 4.6-11.7 1.6-6-3.2-9.2-2.8-9.2-2.8a14 14 0 0 1 14.8.6c4 2.3 5 .4 5 .4-1.2-.7-1-1.5-1-1.5 9.6 4.9 13.8 2 15.9.5s-1-3.4-1-3.4c-.2 3-4 4.6-7.2 3.5-3-1-6-2.4-10.4-4.3s-10-.8-15.1.2c-5.2 1.1-5.9.6-6.4.2s-.7-1.7-3.4-.6c-2.6 1.1-8.8-1.8-12.6-2.7s-10.1-.5-15.5 2.5c-5.4 3.1-8.2 2.3-9.8 1.6-1.6-.8-2.7-2.8-.9-4.6s2-2.3 1.8-5c-.2-2.6-2.8-4.2-2.8-4.2 2.4-2.5 3-3 2.2-4-.8-1.2.4-1.2 1.8-1.8s.8-.7.5-1.5-1.2-.6-1.2-.6c-3.1.1-4.9-.8-4.9-.8-5.2-2.4-10.1 2.3-10.1 2.3-3-2.3-3.7-.7-4.2-.2q-.8.9-3 1c-1.2.2-3.1.7-3.8 1.9 0 0-.6 1 .1 2 0 0 .8 1.2-.6 2.7-1.5 1.5-2 1.8-1.5 3.3q.6 2-.3 3.3s-.7-.7-.5-1.7q.3-1.5 0-2s-1.5 1.4-1.8 2.4c0 0-.6-1.6 1.6-3.7q3.1-2.9 2.4-4c-.4-.6-2 .4-2.3.6z"/>
|
||||||
|
<path d="M448.4 338s-2.7-2-2.4-4.9c.3-2.7.3-3 0-3.7 0 0-.5.3-.4 1.4s-.2 2.1-.3 2.3c0 0-1.3-2.3-2-2.8 0 0 .6-2.4-.2-3.4q-1-1.5-2.4-.8c-1.2.4-2.1 1.5 2 4.8 0 0 1.5 1.3 2.5 3.9s2.8 3 3.1 3.2zm13-7.8s-.1-1.5 1.3-4.3a6 6 0 0 0 .3-5.6c-.3-.8-.6-.5.9-1.7 1.7-1.5-.7-3.4 2.3-6 0 0 1.8-1.6 2.3-2.3 0 0-3 1.6-5.2 2.5-2 .8-9.6 4.6-8 7.1 1.8 2.5 1.6 2.7 1.3 3.8 0 0-4.6-2.5-3-6.4 0 0 .8-1.5 2.7-3.4 1.8-1.6.8.4 4.3-1.7 0 0 2.7-1.6 4.3-3.9 0 0-2 1.2-2.6 1.4 0 0-4 .8-5.8 2.5-1.6 1.7-5.1 4.7-4 8 0 0-4-.4-5-4.7 0 0-7.6 9.4 8.4 13.8 0 0 3 .8 5.6 1z"/>
|
||||||
|
<path fill="#ffc221" d="M531.6 299c6-1 40.4-6.2 43.6-6.5 3.4-.3 4.7-.8 5.9 2 1.3 3-4.8 3.1-4.8 3.1l-41.1 4.7c-2 .2-2.5-.6-2.5-.6l-1.5-2s-.5-.6.4-.8z"/>
|
||||||
|
<path fill="#5a3719" d="M447.3 317.7s-4.4 9.3 13 11.6q-.1-.1.8-2.5c.8-1.5 2.3-4.5.8-6.4s1.2-.9 1.5-3.4c.5-2.5-.2-2.2 1-3.8 0 0-5.4 2-7.6 4.5-2 2.4 2.9 4.2 0 6.9 0 0-2.5-1-4-3.6 0 0-3.3 0-5.5-3.3"/>
|
||||||
|
<path d="M464.5 329.2s4.3 3.7 9.4 3.6c5.1-.3 7.4-1.6 8.7-3.6 0 0 1 1.5 1 2.6 0 0 4.4-3.7 12-.5s5.4 2.3 7.1 2.5c0 0-3.3-.5-10.7 2.9-7.7 3.5-27.7 2.3-27.6-7.5z"/>
|
||||||
|
<path fill="#5a3719" d="M457.3 312.6s1.9.3 3.8-1.9c0 0-2.6.5-3.8 2zM442.6 330s-3.6-2.8-1.3-3.4c0 0 1.7-.3 1.3 3.4"/>
|
||||||
|
<path d="M521.2 347.8s2-3.5 7.5-3.5 6.1 2.6 13.1 3c0 0-8.4 2.4-14.2.3-3-1.1-5.8-.2-6.4.2"/>
|
||||||
|
<path fill="#5a3719" d="M466.3 331.7s8.4 5 15.7-.5q.2 0 1.2 2s5.6-5.4 15.5.4c0 0-1.2-.1-5.9 1.8-6.1 2.7-21.4 4.5-26.5-3.8z"/>
|
||||||
|
<path d="M498.3 336.7s8 1 14.7.6c4.1-.2 8.6-1 6.4.4-2.3 1.3-1.1 1.5 8.4.7 9.4-1-.1 1.7 6.4 2.6 0 0-15.9 8-35.9-4.3"/>
|
||||||
|
<path fill="#5a3719" d="M519.2 331.7s4.6-1.7 9 .3c4.3 2 3.6 2.2 6.5 2.5 0 0-2 2.9-6.7.6s-6-2.8-8.8-3.4m5.2 14.3s4.6-2.3 9.6 0c.6.4 2 1 3.3 1.2 0 0-3.8 1.3-7.8 0-1.7-.5-3-.9-5.1-1.2m-22.7-8.2s10.3 1 15.8-.1c0 0-6.4 3 9.7 1.7 0 0 3.5-.4 3 .1-.3.5-.6 1 1.2 1.5 0 0-12 5.4-29.7-3.2"/>
|
||||||
|
<path d="M450.7 329.2s.2.7 2.4 1.7a9 9 0 0 1 4 3.9 6 6 0 0 0 3.5 2.9s-8 1.7-11.6-2.6c0 0-2.7-3 1.6-6"/>
|
||||||
|
<path fill="#5a3719" d="M513.7 347.6s-3.1-.2-7.5-1.7c-4.3-1.5-5.4-.2-7.9-2-2.4-1.9-7.3-.7-8.2-.6-1 .1-3.6 0-.3-2.1 0 0-2.6 0-3.6-1.4 0 0-1.2 1.2-5.6.8 0 0 2 3-6 2.1a10 10 0 0 0 11.1 3c0 .2-.5 2.5 3 3.5 3.8.9 4.5 1.6 6.4 2.3 0 0 .3-1.5-4.6-5 0 0 2.6-.2 6.4.7s12.2 3.1 16.8.4m2 3.7s.8 1.8 3.2 1.4a17 17 0 0 1 10.2.8s.7-3.2-7-3.4c0 0-4.8.2-6.4 1.2m-65.2-21s-3 2.5-.3 5c2.4 2.3 6.2 2.1 8 2 0 0-1-.6-2-2-1-1.5-1-2.5-3-3.4-2.1-.9-2.3-1.1-2.7-1.6m-3-12.6s-4.6 9.3 13 11.6q-.2-.1.7-2.5c.6-1.5 2.1-4.5.8-6.4-1.6-1.9 1.1-.9 1.5-3.4s-.3-2.2 1-3.8c0 0-5.5 2-7.7 4.5-2 2.4 2.9 4.2 0 6.9 0 0-2.5-1-4-3.6 0 0-3.3 0-5.5-3.3z"/>
|
||||||
|
<path d="M493.3 339.3s3.7-.6 13 2.9c9.4 3.4 13.3 2.6 14.6 2.5 0 0-5.2 2.8-13.4-.8-7.2-3.2-7.6-2-14.2-4.6"/>
|
||||||
|
<path fill="#ffc221" d="M551.8 337.2s2 0 3.4.5c0 0 .7-.7 2.7-1 0 0-1.3-1.2-6.1.5m-6.4-5.2s2.1 0 2.8-1.2c0 0-1.1-1.3-2.8-2 0 0 .4 1.6 0 3.2m-71.7-23.8s-.5-1 1.8-1.4l31.3-4.5s1.5 0 1.7 1c.3 1.1-.1 1.9-7.2 2.7l-25.6 3.2s-1.9.3-2-1"/>
|
||||||
|
<path fill="#ffc221" d="M502 306.9s0 4.1 4.2 4.7c4 .6 5.5-.2 6.5-2.3.3-.7 1.6-5-.2-5.3q-1.4-.2-2.9.3c-1.4.7-2.7 1.4-2.3 2 1 1.6 1.2 2 1 2-1.2.3-1.8-.6-2-1.2-.3-.8.5-1.2-2.2-.8q-1.9 0-2 .6zm17.5-3.2c2 .3 1.9 4.8-.6 6.9-2.8 2.2-5.4 1.3-5.4 1.3-1.4-.5-1.2-.4-.1-2 1-1.5 1.5-3.6.9-5q-.2-.8 1-1s2-.4 4.2-.2"/>
|
||||||
|
<path fill="#ffc221" d="M521.3 304.1s1.6 2-.4 5.5c0 0-.8 1 1.1.9 1.8-.2 6.1-2.2 5.7-4.8 0 0-.2-.6-1.3-.6s-.2-.5.3-.8c.4 0 1.9-.6-1.9-3 0 0-.6-.6-1.3-.3-.6.2-2.6 1-2.6 2.2q.2.9.4 1z"/>
|
||||||
|
<path fill="#ffc221" d="M525.4 300.9s3 2.1 3 2.8-.3 1.5.5 1.3c.8 0 4-.7 3-2.8q-1-3-3.2-3.4c-1.5-.6-1.9.1-3.2 1.1 0 0-.9.6-.1 1m-16.1 3s.5-1.5-2.2-2.2c0 0 1.1-1 3.4-.4 2.2.4 2 2 2 2.1 0 0-1.8 0-3.2.5m5.8-.4s3-.5 4.5-.4c0 0-1.6-3.3-5.7-2.3 0 0 1.5 1.8 1.2 2.7m5.3-.8s0-1.1 2.6-2.1c0 0-1.2-1.2-3-1-2 0-2.5.7-2.5.7s2.3.8 2.9 2.4m1-3.6s1.7.4 2.7 1.3c0 0 1.5-1.7 2.8-2 0 0-2.5-1.4-5.5.7"/>
|
||||||
|
<path fill="#5a3719" d="M435.8 290.9s7.2-6.2 11.2-5.4 2 .2 6.4-.5 9-1.1 10.8-.9c0 0-5.4-3.8-14.9-3.7 0 0-6.6 2.3-11.3 5.3 0 0-8.9-4.9-18-2 0 0 9.9 3.7 15.8 7.2"/>
|
||||||
|
<path fill="#ffc221" d="m512.2 301.4 1.2-.2s2 2.5.6 2.5c-1.2 0-.8-.3-1-1a2 2 0 0 0-.8-1.3m-9 .2s-.8 1 .6.8c1.7-.3 1.4 0 3.1-1.3 0 0 1.2-1.1 3.2-.4 0 0 1.8.6 3.2-.1 1.4-.8 1.7-.7 2.5-.6s.8.2 1.7-.6c1-.7 2.8-.1 3.9-1s2.5-.2 0-2c0 0-.5-.5-.5-1 0 0 1 .4 1.8 1 .8.8 2 .5 2.2.4 0 0 .2-2.3 2.3-4.3 2.3-2 2.3-2.2 1-2.2s-3.5-.6-4.3 0-7.2 4.8-11 5.5-7.3 1.8-9.7 5.8m-101.3-23.4s11.7 3 14.3 4.2c0 0 .6-1.9-4.7-3.4 0 0 12.9-.4 26.4 5.8 0 0 6.6-5.6 27.7-3.9 0 0 0-1.8.2-3.3 0 0-14.8-.4-28.4-8.7 0 0-12.3 6-35.5 9.3m64.7 5.6c-.7-11.8 3.8-13 3.8-13s2.1 0 4.4.5c0 0-3.6 4.3-2.6 12.8 0 0 .4 1.3-2.7 1.3s-2.9-1.5-2.9-1.5z"/>
|
||||||
|
<path fill="#5a3719" d="M469.8 291.7s-2.3-2.3-2.5-4.9c0 0 0-.6 2.2-.6s2.5-.2 3 1.1 2 4 2.3 4.3z"/>
|
||||||
|
<path fill="#ffc221" d="m474.5 285.7-.2-4.5c.1-6.6 1.2-6 1.7-5.2h2.3s-1.7-7.4-3.7-3a19 19 0 0 0-1.5 10.4q.1 3 .6 4z"/>
|
||||||
|
<path fill="#5a3719" d="M500.2 285.7s4.3.8-2.3 2.3c0 0 .3 8.2 8.2 2.5 0 0 4.7-3 8-4.2 0 0 1.6-.6 1.4-1.8 0 0 .2-1.5-1.5-1.1 0 0-1.4 0-2.3-.3 0 0-1-1.2-1.6-.8-.6.5-2.1.2-.9 1.7 1.2 1.4 1.5 1 2 .6s3.1-1.4.9.7-4.2-1.2-5-1.8zm-22 1h-2s-1 1.6-1.7-1l-.7 1.6s2.3 8.8 4.4-.6"/>
|
||||||
|
<path fill="#ffc221" d="M475.4 276.6s-1 5.8.3 9.2l21.1.5s-.2-4 0-9.7H494s-.5 4.6 0 7.5h-.5s-.4-4 0-7.5H491s-.4 4.3 0 7.5h-.5s-.4-3.7 0-7.5H488s-.5 3.9 0 7.5h-.6s-.5-3.9 0-7.5h-2.7s-.6 3.6 0 7.5h-.5s-.6-3.6 0-7.5h-2.7s-.6 4.2 0 7.5h-.6s-.4-4 .1-7.5h-2.5s-.7 3.5 0 7.5h-.7s-.4-3 .2-7.5zm22.3 10.4s-.5-10.2 1.4-13c2-2.6 2.5-2 5.8 0 3.4 2.2 7.7 4.5 8.5 4.8.6.3 1.6.5 1.6 2.4s.3 2.4-2.6 0a9 9 0 0 0-2.7-1.8c-2.6-.9.6.5 1.5 1.9.8 1 1.5 1-.6 1.5a219 219 0 0 0-12.9 4.2"/>
|
||||||
|
<path d="M505 279.6s-1.5-1.8.5-2.3 2.1 3 2.5 5.1c.3 2.2-2.5-2.1-2.8-2.7zm-2.7 9s-2.3.9-.7 1.6c1.4.7 5.5-2.7 4.2-2.5-1.6.3-3.5 1-3.5 1zm3-3s2-.3 1.6.5c-.3 1-1 .4-1.4.2s-1.6-.7-.1-.8z"/>
|
||||||
|
<path fill="#ffc221" d="M516 282.8s.6 4 4 5c0 0 2 .4 1.5-1.3 0 0-.3-1.5-.6-2-.3-.7-1.6-1-1.8-1.1-.2 0-.3-.5.6-.2 1 .3 1 .4 1-.3s-.6-.4-1.4-.8c-.4-.2 0-.4.3-.3.4 0 1.3.3 1.3-1 0 0 .1-.8-.9-.8-1.1 0-1-.6-.7-.7s1.5.8 1.9-.6-1.6-.5-1.4-1.2c.3-.8 1.7.3 1.7-.5.2-.8 1.3-1.1-.6-1.4-.9-.1 0-.6 1-.4s1.6-1.2 2.3-1.6 4.2-2.6-.6-1.9-6.1 3-6.3 3.5a13 13 0 0 0-1.3 7.6"/>
|
||||||
|
<path fill="#ffc221" d="M527 285.8q1-.1 1.4.5c.8 1.6-1 1-2 2.2s-1 1-2.4.5-2-2.5-2-2.5q0-1 1.2-.6s2.3.2 3.9 0zm-5-.8s0 .4.9.5c.7 0 3 .3 4.5-.1 0 0 .4-.1.2-1 0 0 0-.7-1.2-.4-1.3.2-3 0-3.7-.1q-1-.4-.8 1zm-.2-2.9s-.1 1.3 1.1 1.4c1.3.2 2.9.2 3.5 0 .5 0 1.4-.2 1.5-1 0-.7.2-1.2-1.3-.8s-3.4 0-3.6 0c-.1 0-1.2-.3-1.2.4m.5-2.5s-.3.6-.2 1.2q0 .7 2.5.7c2.5 0 3-.2 3.2-.7.1-.7.5-1.3-.7-1-1.3.1-3 .2-3.6 0q-1-.5-1.2-.2"/>
|
||||||
|
<path fill="#5a3719" d="M582.1 286s0 1 .9 2.2l-45.2-1.3s.6-.4.8-2.2z"/>
|
||||||
|
<path fill="#ffc221" d="M522.7 277.8s-.4 1.1.4 1.4q1.2.4 4 .1s1 0 1.3-1 .3-.4-2.3-.8c0 0-.8-.3 1.5-.3 0 0 1.4 0 1.5-.2.3-.2 2-1.7-.3-1.5-2.3 0-1.1-.5 0-.5s1.6.3 2 0 0-.2-.7-.8-.1-.5.3-.1q.6.6 1.3 0c.7-.6-.4-1.2 0-1 .3 0 .6.8 2 0 1.6-.7 3.5-.3 4 0 .6.5 2.2 1 3.1 0 1-.8-1.1-1.7-.3-1.8 1-.1 1.6.2 1.9-.5.4-.8-1.4-1.4.3-1.8s.2-5-.3-5.5c0 0-1.9 1.1-3.8 4.3-2.1 3.3-3.3 5.2-6 4.2-3.9-1.5-6 .6-6.5 1-1 .6 2 .8.2.9-1.7 0-1.7.2-1.8.4q-.2.4.3.6c.3 0 .9.6-.1.6s-1.8-.3-1.5.9l.6.3c.6 0 .8.8-.3.8q-1 0-.8.3m4.1 11.3s-.7.5.3.6c1.2 0 1.7.3 2.1-.3s1.8-.4.8-1.2-1.6-.3-3.2 1z"/>
|
||||||
|
<path fill="#ffc221" d="M531.5 275.5s3.8-3.5 6.9-1.2c3.2 2.5 3.4 2.8 3.5 2.9 0 0 .4.3-.4 1-.9.8 0 .8.9.3s1 0 1.4.5c.5.5 1.1.8-.3.8h-4.6s-2.1.2-1-.7c1-.9.8-1.9.3-2-.6 0 0 .6-.3 1q-.6.6-1.9.7-2 .3-.2 1c1.1.4-.2.7-.8.7s-3.5.2-.5.6-.3.3 2 1.5c2.4 1.4.6 4.3-.3 4.6 0 0-1 .5.2.4 1.3-.2 2-.3 1 .4-.8.6-2.6 2.9-5 1.2 0 0-1.2-.6.8-.7s-1.6-.5-2.3-1c-.5-.3-3-2.7-1.5-2.5 1.6.4 1-.5.1-.8s-1-1.6 0-1.4 2 .9 3 .8q1.2 0-1.2-.8c-1.7-.6-2.4-.7-2-2 .4-1.5 2.3.5 1.8-.6-.4-1-2-.5-1.2-1.9s1-.8 1.5-.6c.3.1 1 0-.1-.8-.8-.5 0-1.3.2-1.4"/>
|
||||||
|
<path d="M534.2 276.5s0-.5.8-.4c.6 0 .4-.2.6-.4.2 0 1.9.5.3 1-.6.3-1.6.2-1.6-.2z"/>
|
||||||
|
<path fill="#ffc221" d="M537.9 280.5s-1.3.6-.2 2c1 1 1 1.5 1 2.2-.1.8 43.4 1.3 43.4 1.3s0-2.9 1.8-4.5z"/>
|
||||||
|
<path fill="#5a3719" d="M582.8 285.2s.2-2.4 1.6-3.1c.7-.5 1.6-.3 2 1.6.6 2.7-1.7 5.1-2.7 4s-.8-2.5-.8-2.5z"/>
|
||||||
|
<path fill="#7b3c20" d="M532.9 295.4s2.9-2.5 3.4-3.6c0 0 7.8 5.6 7.3.4l.2-2.6s2.9.3 3.3-2l-7.3-.3s-.8-.1-2 1.1c-1 1.2-3.4 2.5-5.5 1.4 0 0-1-.8-1.9 0-1 .5-1 .7-.2 1.5s2.4 2.9 2.7 4zm16.8-15.4-4.3-.2s-1.5-2.2-4.6-4.6c0 0-.9-.4.8-1.8q2.5-2.3 2.3-3.5c0-.6 0-1.7.6-1 .6.8 5 4.9 5.8 3.7q.8-1.6.7-2.1c.2-.4.3-1.5 1-.3s1 .8 1.1 3.8c0 0 0 3 .5 4 0 0-5.6-1.7-3.8 2zm-18.6-9.2s3.3 2 5-.6c1.5-2.5 2.6-2.8 1.4-5.3-1.2-2.3 0-3.4.9-4.4s1.8-.8 1.8-4.6c.2-3.9 2.8-5 4-6.3s4.2-3-.4-3.7c-4.4-.8-13.4-3-15.7-6.5s-3.3-1.5-3.3-1.3-.8 2.7 1.5 7.3 4.2 7.6 6.5 9 4.2 2.3 3 5.4-3 8.6-4.7 11"/>
|
||||||
|
<path fill="#5a3719" d="M543.2 261s.6 8 6.3 10.8c0 0 1.3-3 .8-6.1 0 0 1.9.1 2.4 1 0 0 0-2.3-2.6-3.2-2.7-.7-1.4-6-.4-6.5s.6-1.7 0-2.6-.8-2.3 1.4-1.7 2-.6.5-1.7c-1.3-1.1-1.3-2.5.7-2.5s5-1.9 3.2-2.4c-2-.6-2.5-1.3 0-2 2.7-.8 4-1.7 2-2-2-.2-3.3-.9-1.4-1.2s-.3-2.3-2.5-2.3c-2.3-.2-7 .7-3.3-2.3s-5.4-.8-1.6-2.8-1.3-1.1-2-1.1-.7 0-.4-1-.5-1.6-1.7-.9c-1 .6-1 .6-1-.7 0-1.5-1.3-.4-2.1 0-.9.3-3 1.9-3.9 1-.7-.9-1.2-1.7-3.8-.2s-2 .2-2-.5 1-3.4-2.4-.5-.7-3-3.5-1-3 2.4-3.5 1.5c-.5-1-1-1.7-4 .3-3.1 1.9-.8-1.3-.5-2 .5-.6 1.8-5-.9-1.6 0 0-1.3 2.4-4.2-1.9 0 0-3 4.3-3.9 2.4-.8-2-1.5-2-2.6-.8-1 1.2-.2-.1-.7-1.2s-.7-2.9-5.8.8 1.8 1-2.1 2.7-13.5 7-4.8 5.9c8.7-1.3-4.2 3.3-1.2 4.2 3 .8 2 3.5 13.4.3 11.2-3 9.4-.4 15.2-3 5.8-2.4-1.4.9 6.4.8 7.7-.2 1.3 0 2.8 1.6s8 5.3 14.1 6c6.1.6 7.7-1.7 5.9 1-1.8 2.6-2.4 3.6-3.4 4.6-1 .8-4 3-4 6.6 0 3.7-4.8 4.3-3 8.3l4.1-4z"/>
|
||||||
|
<path fill="#5a3719" d="M553.3 269.9s-1.4-1-1.4-2.8c0 0 1 .2 1.4.8 0 0 3.5-4-.8-5.4-4.1-1.4-2-5.3-.6-5.3s1.7-.3.4-2c-1.2-1.5-1-1.6 1.3-2s2.1-1 1-1.5l-1.9-1.6s6.8-2.9 4.6-4.3 0-1 2-2.3c2-1.4 2.2-1.7 2.5-2.3 0 0-2 .3-3.4 0 0 0 1.7-.9 0-2.3s-2.3-2.6-5-2-1.8-.2-.8-1.3.6-1.7-1.3-2c0 0 .2-1.2 1.7-2.5 0 0-3.7.2-5-.4 0 0 1.6-1 1.6-2.3 0 0-2 .7-4.5.5 0 0 1.5-1.3 1.5-2.4 0 0-4.4 1-6.4 2.5q-.2.2-.8-.6c-.4-.4-.6-1-5.5.6 0 0 .5-2.2 1.7-3s1-2.6-6.5 2.1c0 0-1-.6-1.9-2.9 0 0-1.7 2.3-2.9 3.1 0 0-1 .5-1-1 .2-1.5-.7-.5-1.4 0-.8.4-1.3 1.5-1-1.5s-1-3.6-1-3.6-2.3 3.3-3.7 3.7c0 0-2.5-2.4-3.4-4s-.8-2.2-1.7.6c-.9 2.7-2 3-2 3s-1.5-1.3-1.6-2c0 0-.3.7-.8 1 0 0-1.3-1.5-1.2-3.7 0 0-8.2 4.5-9.2 7.2 0 0-7.7-.5-10.8.1 0 0 .7-2.4 2.7-3.7 0 0-2-.2-2-2.3 0 0 1.6.2 2.6 0s-1.4-3.1 1.1-3.2 4.2 1.2 3-2.2c-1-3.3-.6-3.3-.6-3.3s4.4 2.6 5.1 1.9c.8-.6-.5-2 3.4-1.4s2.8-1.5 4.4-1.7c1.5 0 2.3 1 1.3-6.1s4.8 3.5.9-7.2c0 0-1-3.3-3.3-4.7 0 0-.6 2.3-3.2.3s-7.8-2.8-5.6-4.4 3.2-3.9 2.6-5.2c0 0-2.6 2.6-7 .7-3.6-1.5-4.4 1.3-8 .5 0 0 0-1 3.1-3.4 3-2.3-1.8.8-3.6 1.3s-2.4 0 1.5-3.1c4-3 12-8.5 11-13 0 0 1.8 2.3 6.7.6s8.6-2.3 10-5a23 23 0 0 1 6.4-5.6c1.1-.5 2.4-1 .9 1.5-1.6 2.4-4 6.6-10.8 9.4s-9.5 4.8-10.7 6.3-7.4 4.8-3.3 4.3c4-.7 10.9 0 7.6-1-3.2-.9-6.9.6-3.9-2.1s3.5-3.6 7.9-5.4 9.2-6.1 8.7-1.6-8.6 9.1-10.6 10.6c-2 1.4-1.2 1.2-1.2 1.8s-.4 1.8-1.2 2.3c-.8.6-.5 1.2-.3 2.4.2 1.3-.2 1.8.4 2s1.2.2 1.4 1.1.6 1 1.8 1q1.8-.3 2 .6c0 .7 1.2 1.7 1.3-.4.1-2.2 1-2.5-1.2-1.5s-2.6.6-2.6-.4-.2-.8-1-.9c-1 0-1.3-1.3.3-2.2 1.6-.8 1.6 0 3.6-1.6s2-2 2.3-3c.3-.7-2.9 2.4-4.4 3-1.5.8-1-.5-.8-2.1.3-1.7 4-4 5.7-4s5.6 1 4 3.3c-1.7 2.3-6.4 5.2-4.5 5.4 2.1.2 2.4-.6 3.6.4s0 3.2-.4 4.4a8 8 0 0 1-2.2 2.7s-2.2-3.8-2.1-.8c0 3.1-.5 4.2 0 4.3s2.8 1.7 3.6 1.7-4.1 2.3-2 2.5c2 .1 5.3-1 6.4-3 0 0-4.2-1-5.9-2.6 0 0 4.9-1.2 3.5-5.8 0 0 4.9 1.3 2.7 3.5-2 2.1-3.3 1.8-1.5 2.4q2.8 1 2.7 1.2c-.1.2 1.3.6.5 1.6-.7 1-.7 2.6 0 2.5.5 0 2.6-1 .9-2s1.9-.8.4-1.7c-1.6-1-2-1.1-2.4-1.6-.5-.3 19.7-12.2 9.5-7.8 0 0 2.1-4.6 5.1-4.6s3.2 2.3 1.5 4.2c-1.7 1.7-2.8 4.6-6.7 5.2 0 0 5.6 2.7-1 7.2 0 0-1.5.7-1 1.2s4.5-1.7 5-3a6 6 0 0 1 3-3 38 38 0 0 0 11.2-9.6c2.3-3.9 2.8-4 7.2-7.5s3.6-2.8 4.2-3.6c.5-.9.7-2.3 2.7-3.4s9.8-5.4 12.3-7.2c2.4-1.8 7.4-5 9.6-7.8s8-6.2 9.4-5.6-.2 2.8-3.5 5.4c-3.5 2.5-12 9.3-13.3 10.4a45 45 0 0 1-11.2 6.6c-2.7.3-2.4 1.3-4 3s-5.3 5.4-6.5 6.4-4.3 3-4.4 4.5.5 1.6-1.9 3.8a50 50 0 0 1-11.9 8.1s4.5 1.6 1.8 4.6c-2.6 3-2.5 2.6-2.6 2.8 0 0 6.7-1 2 4.3 0 0-1 1.5 1.1 0 2.3-1.9 1.4-4.1 1-4.5 0 0 3.7-2.3 7.9-2.3q6 .1.2-1.4s2.7-3.2 5-1.6c2.2 1.5 1.4 2.5-.9 3.8-2.4 1.2-5.8 1.7-8.5 3.2 0 0 5 1 7.6-1.1 2.6-2 2.8-1 3-.6.5.4.8 1-.4 2.6s-1.3 1.8-1.2 2.2c0 .4-.1 1.5-2.5 2-2.3.3-3.5 1.3-2.6 2.4.7 1.2.7 4-1.2 3.7-2-.3-1.6-1.9-2.3-2.5-.8-.6-1.9-1.5-5.4.2-3.6 1.9-3.8-.3-3.7-1.5 0 0-2.3 2-4.2.2s-.2-2.6 1-3.5c1-1 5.5-3 2.8-2.5-2.7.3-6.7.4-7.6-1.6-1-2.1 2-1.9 2.4-1.7.5.2 2.3 1.7 2.5-.3 0-2 3-2.3 2-2.6s-2.5.9-2.9 1.3c0 0-2-2.9-5.4-2-3.4 1 1 .7 2 .8.8.2.3 1.8-2.7 4.6s-1.7 1.8.5 1.8 7.9 0 4.6 2.6c-3.2 2.7-4.5 4-6 3.6-1.8-.5 0-1.6.8-2.1s1.2-1.2-.4-.6-2.1.7-3.4-1.5-.8-1.6-.2-3.1 1.8-3 .4-2.5c-1.6.6-1.4.7-1.3-1s-1.7-2.1-1.7-2.1.8 1.7.1 2.8q-1 1.4.4 1.6c1 .4 2 1.3.6 2.3s-1.2.7-.4 1.3c1 .7 2.3 1.3.9 2.7s-.3 1 .4 1c.8 0 2.3.6 2.3 2 0 1.2 0 1.5 2.3.3 2.3-1.3 6.7-1.1 6.7.6s-.6 2.3 1.8.8 3.5 1.4 5.2 0c1.6-1.5 2.6-2.8 4.6-.4s1.3 3-1 4.8 1.1.4 2.9-.5 6.7-1.5 9.5-.2c2.9 1.2 3.7 1 5.8 0 2.1-.8 3.2-1 6.3 1.1 3.3 2.2 5.7 2.6 7.4 2.5 0 0-3.5 1.4-7.5 1.6q-5.8.6-6.7 1.6s2.3 1.5 2.8 3.2c0 0 2.6-.3 3.8.2 0 0-.6 1.9 1 2.9s2.7 1.4 1.5 2.7 1.9.8.1 2.7-2.1 3-2.2 4.5c0 1.6.4 1.8-1.1 2-1.6.1.2 1.9-.5 4-.7 2-5 1.7-4.8 7.2 0 0 1.2-2.7 3.8-5 2.5-2.4 2.6-2.6 2.5-4 0-1.4-.1-1.1 1.2-2.2 1.4-1-.6-2 .8-3.6 1.3-1.5.2-1.2 1.8-2.7 1.5-1.6-1.5-1.7.2-3.3 1.5-1.6-4-3.5-2.3-4.5 1.5-1 4.3-2.4-5-2.3 0 0 2.3-3.6 10-2.9 0 0-2 1.6-2.2 3l1.6.5s-.4 1.1-1.9 2.3c0 0 4.2 2.3 4.9 3.8 0 0-2.6.8-3.3 1.8 0 0 1.2 1.3 1.6 3 0 0-2.9-.4-3.2 1.7s-1.3.7-1.3 2 .1 1.7-.9 2c-1 0-.1 1.1 0 1.8.2.7.6 2.3.4 2.8 0 0-1.5 0-2.1.2 0 0 .4 3-1.3 3.5s1 1-.9 1.3-1.5.5-3.7 4.5c0 0 1.9-1 3.8-2.4 2-1.3-.2-1 3-4 3.3-3.3 2.7-3.5 2.4-5.1-.2-1.6-.3-3 .9-4.5s1.5-3.2 5.7-3c0 0-1.2-2.8-2.7-3.5 0 0 2-1.3 4-1.5 0 0-1.8-2.3-5.6-4.4 0 0 3-2.6 3.9-3.9 0 0-1.5.3-2.7 0 0 0 .6-1.3 3.4-3.1 0 0 1.5 1.4 1.4 2.9 0 0 4.8-2.7 7.5-2.4 0 0 1.4 3.4-5.3 10 0 0 4.2.3 6 0 0 0-1 3.2-6 5-5 2 1 4.2-4.1 3.8s-3.5 1.3-3.4 3.9.3 5.3.2 6c0 0-4-1.3-4 2.6.1 4-2 4.8-2.5 5.1 0 0-1.2-1-3-1.7 0 0-2.5 4.9-6.5 7.7"/>
|
||||||
|
<path fill="#7b3c20" d="M547.4 220.5s1.4-.2 3.8 1.2c2.3 1.5 4.6-1.5 2-2.3s0-1.8 2.4.2c2.4 1.9 3.3.9 4.2.3.8-.7 1.9-1.1.3-2.2s1-.6 2.3.3c1.2.7.7 1.5.6 1.7-.2.2-.3 2.9 2 .5 2.4-2.5 3.7-4.8 3.6-6 0 0 1.3.8 1.5 2.3s2-.8 2.6-1.6 1.6-3 1.5-4.4c0 0 1.6 2.5 4 0s1.4-1 4.2-1.7a18 18 0 0 0 8.5-5.2c2-2.5 2.1-.8 4.6-1.4s7.7-4.2 8.2-6.1.3-3.1-.4-2.4c-.7.6-.4 0-1.5-.6s-2.7.9-2.7.9 1.6 1.2.3 1.7c-1.2.6-2.3 2.3-4.6 1.6s-4.8 2.2-4.8 2.2 2 1.6-.7 2.7c-2.7 1-2.3 1.4-3.9.2 0 0-2.9 3.7-4.6 4.5 0 0-.7 0-1.2-.8 0 0-2 2.1-2.8 2.5 0 0-1.3-1-2.3-1.5 0 0-2.3 2.9-4.2 3.7 0 0-.6-1-1.8-1.7 0 0-.6 3.6-4.6 5.8 0 0 .2-1-1.8-2.3 0 0-5 4.3-6.9 4.7s-.2-.9 0-1.5c.4-.5 1.6-2.3-.8-3s-2 .5-2.5.7-.6-.4-2.2-.2-1.3.9-2 1.2c-.8.2-3.6-.5-3.4 1.4.1 1.8 1.5 3.1-1 4.2-2.5 1 1 .8 4.1.4"/>
|
||||||
|
<path fill="#5a3719" d="M557.5 215.3s.6-2.5-1.5-3.5c0 0 13.2-2.1 3.2-7.2 0 0 11.9-2.3 9-6.1-2.7-3.8-5.4-3-5.8-3s2.5-2.1 3.3-1.8 10.2 3.9 7.8.7c-2.4-3-2.2-2.9-2.6-3.8 0 0 3.1 0 7.9 4.6 0 0 1-1 .9-2.9 0 0 3.3 1 4.4 2 0 0 .6-1.2.3-1.8 0 0 3 1.5 4 3.2 0 0 1.3-1.2 1.5-2.6 0 0 3 1.3 3.7 2.2 0 0 1-1.3.6-3 0 0 4.9 1.3 5.5-1.6 0 0 4.9 1 1.7 2.9-4 2.5-.4-.6-4.6 2.3-3.2 2.3-5 4.9-6.6 4.4-1.1-.5-2.5 2.9-4 1.3-1.5-1.7-1.5-1-2.7.7a25 25 0 0 1-2.8 3.5s-.8-.5-1.6-1.2c0 0-.8 1.6-2 2.8 0 0-1-1.3-2.6-2 0 0-2.3 2.7-3.8 3.7 0 0-1.4-1.4-2.9-1.9 0 0-.2 3.8-3.1 5.7 0 0-.6-1.2-2.6-2 0 0-1.4 2.3-4.6 4.3z"/>
|
||||||
|
<path fill="#5a3719" d="M550.6 209.5s-1.6 1.2-.6 2.5 1.1-.2 2.4-.3c1.3-.2 17.5-3 2.8-7.3 0 0 .7-.6 3.1-.8 2.6-.3 11.8-2.7 7.5-6s-7.9 1.1-4.3-2.8c3-3.1.6-4.6.6-4.6s-8.5 5.6-10.4 6.7c-1.8 1-4.6 3-1.4 4 3.3 1 5.4-3.4 5.7-2.4s-6.4 4.8-5.4 6.4c.9 1.8.7 3.3 2.4 2.9s6 .8 2.5.7c-3.7-.1-5 1-5 1z"/>
|
||||||
|
<path d="M556.4 201.3s-1.5 1.1.5.6 5.9-1.4 5.2-2.4-3.4.2-5.7 1.8"/>
|
||||||
|
<path fill="#7b3c20" d="M582.4 184.5s7.5-.2 10.5 1.9q4.3 3.1 5.5 3.8s-.1 2.8-5 .7c0 0 .4 1.4-.2 2.8 0 0-1.7-1.2-3.8-1.7 0 0-.4 1-1 1.7 0 0-2.1-2.2-4.6-2.9 0 0-.4 1.1-.8 1.6 0 0-2.6-1.6-4.6-1.6 0 0 .4 1.7 0 2.3 0 0-5.4-4.3-10.3-3.8 0 0 2.3 3.5 3.8 5.1 0 0-9.8-.7-8-6 1.5-5.3-.2-4 6.2-4z"/>
|
||||||
|
<path fill="#5a3719" d="M536.3 199.1s-1.1 1 0 1.7c1.1.8 5-2 5.5-2.4s2-.4 0 1.1-3.8 3-5 4.6c0 0 6.4-1.8 10.6-5.4 4.3-3.7-.1-1.3 7-4.8 7.3-3.5 11.1-9.1 7.2-8.5s-7.3 5-10.4 6.7c-3 1.7-4.7 2-4.2 1s2.7-.6 6.9-4c4.1-3.3 3.2-3 3.2-4.2s-1.5-4 4.7-7.4c6.2-3.3 25.6-14.5 27.3-18.5 0 0-5.7.6-13.2 6.1a70 70 0 0 1-13.4 8.8c-2 .8-1.8.2-3.1 1.9a172 172 0 0 1-10.1 9.8c-1.3 1-1.8 1.6-1.9 3.8 0 1-8.4 7.3-11 9.7z"/>
|
||||||
|
<path fill="#5a3719" d="M562 184.3s-1.5.6-3 0c-1.3-.7-.8-3.6 2.5-5.5a50 50 0 0 1 12.6-4.8s-.5 3.8-10.1 7.1c0 0 .6 2-2 3.2"/>
|
||||||
|
<path fill="#aa5323" d="M565.4 181.8s.3 1 0 1.8c0 0 17.9 1.7 27.1-9.2 0 0-12.7 1.2-17.7 4.3 0 0 3.2-4 12.7-7.3s13.4-7.4 14.2-9.7c0 0-12 4.3-17.7 4.3 0 0-1.2 0-2.3.5-1.1.7-8.8 6.2-10.8 7.2 0 0 4.3-.4 5.9-1.8 0 0-3 8-11.4 10z"/>
|
||||||
|
<path fill="#5a3719" d="M531 192s-2.3 1.7-1.3 2.4c1 .9 2.6 1 6-1.9 3.6-3 12.1-10.2 6.8-10.5 0 0-7-.4-6.7 3.8s-4.4 6-4.8 6.2m-15.9-2.5s4.6 2.7 2.8 4.9c0 0 14-11.8 10-14.4-3.8-2.6-6.9 2.3-6 2.7 1 .6 3-.4 2.3.6a77 77 0 0 1-9 6.2zm-3.6-3.8s3 1 3.2 2.3c.1 1.2 9.2-6.4 6.8-9.7-1.1-1.5-6-2-6.4.8-.2 3 4.6-.3 3 1.8-2.2 2.7-5.9 4.4-6.6 4.8m32.6-6.4s-1.9 1.4-.1 2.3c1.8.7 2.8-.6 3.7-1.3s5.3-4 6.2-6 2.5-2.7 4.1-3.7 12.6-6.6 19.5-12.7c6.8-6.1 4-4.5 11-8.4s11.7-7.5 13.2-11.8c0 0-3.3 1-6.2 3l-10.7 6.5c-1.3.5-3 .6-4 1.6s-1 2.3-4.3 5c-3.5 2.9-21 15.3-23.2 17z"/>
|
||||||
|
<path fill="#aa5323" d="M530 183.4s2-1 5.6-.8 17.8-13.6 22-16.3a342 342 0 0 0 18.7-13.8c1.8-1.8 2-3.6 3.6-4.6 1.5-1 3-.9 6.4-2.9s20.3-12 19.3-17.8c0 0-25 15-30.7 19.8a375 375 0 0 1-24.7 17.7c-2.8 1.9-5 5-9.9 8.8-4.7 3.9-9.5 7.2-10.3 10z"/>
|
||||||
|
<path fill="#aa5323" d="M524.8 178s4.6-.4 5.2 1.9c0 0 10-6.8 12.2-9.6s-.8-1.2 4.9-4.9a594 594 0 0 0 27-19.1c2.7-2.3 7.8-5.5 11.9-8.2 4-2.8 19.9-10.6 18.1-17l-14.2 9.5c-2.7 1.8-3.9.8-6.5 2.9s-8.5 6.3-9.5 7.6a161 161 0 0 1-14.4 11c-4.4 3-14.1 8.6-18.9 12.8l-15.8 13.2z"/>
|
||||||
|
<path fill="#aa5323" d="M510.4 176.8s2.3 0 3.1.9c0 0 4.3-3.9 8.9 0 0 0 16.8-11.5 18.5-14.4 1.7-2.8 4.5-2.9 11-7.6 6.6-4.8 10.7-6.9 15-10.2 4.4-3.4 8.1-7.3 11.2-9.3s11-7.2 9.8-11.7c0 0-6.5 3.6-10.6 8.2-4.2 4.6-3.8.7-8.1 4.5a83 83 0 0 1-16.3 11.7c-5.5 2.7-2.2 2.4-6.2 5-3.9 2.5-3.6 2-5 2.5a10 10 0 0 0-5.1 3c-1.6 1.7-5.4 4-9.6 6.5a107 107 0 0 0-16.6 10.9"/>
|
||||||
|
<path fill="#aa5323" d="M515.5 168s-1-1.9.7-3.3 4.6-4.8 5-7 .1-1.9 4.8-3.8a188 188 0 0 0 38.2-21.6c1.8-1.5 6.4-4.6 8.3-6.2 0 0 .8 2.5-1.2 4.2a222 222 0 0 1-21.5 14.9 77 77 0 0 0-9.6 5.5c-1.9 1.6-1.6 2-10.2 6.3-8.5 4.1-9 4.6-8.7 4.9s4.2-1.3 6-2.4 8.8-4.3 11-6a69 69 0 0 1 7-5 296 296 0 0 0 18-11.2c3.5-2.7 4.5-3.5 5.3-3s2 .4.4 2-6.7 6-8.7 7.3-8.1 5-9.8 5.8c-1.7 1-2.4 2.5-3.4 3.2s-3.7 2.7-7 3.5-4 3.3-6.3 4.8c-2.3 1.4-18 10-18.5 10.3 0 0 .9-1 .2-3.2"/>
|
||||||
|
<path fill="#aa5323" d="M570.3 132.4s-.9.8-.4 1.2c.6.6 2.8 2.2 5.6-.6a107 107 0 0 1 12.5-10c2.3-1.5 3.6-2.8 3.5-4.7 0 0-11.4 6-21.2 14.1m15.6-1s1.7-2.9 6-5.7c4.3-2.7 10.8-6.7 11.5-7.6 0 0 1.6 1.7-1.7 3.8l-10.8 7c-.7.7-2 1.6-5 2.5"/>
|
||||||
|
<path fill="#7b3c20" d="M499 163s-4.8 2.6-3.1 4.2c1.7 1.5 4.2 1 5.4.6l3.2-1c.4 0 4.5-1.3 5.6-3.2s3.8-4.2 6-5.8 3-3.2 2.7-4.3zm-28.4 22s3.4-2 8-.7c0 0-.2-1.1-1-1.7 0 0 5.7-1.5 6.9-4s1.5-2 2.5-2.6c1.2-.8 8.7-6.8 7.8-8.1s-1-3.1-1.7-3.7c0 0-1.5 2.1-9 5.7-7.2 3.6-15.4 6.2-21.4 14.2s-5.3 12.6 2 14.7c0 0 5-3.2 17.6-2.1 12.4 1.1 16.6 5.8 17.4 6.6.8.9 3.3 4 .9 9.1 0 0 2.5 1.1 2.6-1.3.3-2.3.4-1.8 1-1.5s1.3.5 1-1.4a18 18 0 0 0-2.3-7.2c-1.1-1.5.2-.8.9-.6s3.3 2.5 1.8-1.5c-1.5-3.8-2-2-2-1.8 0 .3-.4 1.2-3.8-1.4a27 27 0 0 0-8.8-4.4c-2.3-.6-.7-.6.7-1 1.4-.6 3.1-.8 3.8-2.4 0 0-1.4.3-3.8-.6a13 13 0 0 0-11.5 2s1.2-4.5-2.5-4.3c-3.6.3-6.2.2-10 3.2 0 0-.3-4.6 3.4-7s3.2-1 5.2-1.6 2.3-2.7 1.4-3.4c0 0 4.8.9 12.8-5.8 0 0-4.3 5.7-9.5 6.8 0 0-.9 3-5.7 3.7s-4.6 3.4-4.6 4.1z"/>
|
||||||
|
<path fill="#5a3719" d="M457 212.7s2.2-14.1 15.5-15.1c11.4-1 15.1.5 17.4 1.3s8 2.4 5.8 4.2-3.5 1.5-3.5 1.5 2.5-2.9.2-3.3c-2.3-.5-2.4.9-2.7 2-.4 1.3-.5 2.6-1.6 3.6 0 0-1.2-1.4-2.9-.2s-.2 1.2.5 1 1.5-.5 1.3.5-1 2.8-3.8 4.2-2.6 1.3-5.8 1.9c-3.3.5-6.3 1.8-10.5 5.3-4.2 3.6-8.7 2.4-9.6-1.5-.8-3.4-.4-5.4-.4-5.4z"/>
|
||||||
|
<path d="M472 212.2s1.2-2.7-1-4c0 0-6.9 1.2-9-.9 0 0 7.5-.4 12.2-2.3 4.6-1.7 3.3-3 1.7-3.4s-4.6.5-4.9 2c0 0-1-1.6.2-2.6a5 5 0 0 1 4.7-.8c1.7.4 3.1 1.2 8.6-1.6 0 0 3.1.7 3.3 2.8 0 2.2-.3 3-.6 3.3q-.3.8-1.3 1c-.6-.2-1.5-.3-2.3 1.2a9 9 0 0 1-2.6 3.7s1.5-4.6-2.5-5.7c0 0-3.3 2-5.8 2.1 0 0 3.2 3-.8 5.2z"/>
|
||||||
|
<path fill="#5a3719" d="M479.3 203.8s-1.6-1.6.4-1.9c2-.1 4.6 1.4 4.2 2.7-.6 1.2-3 1.1-4.6-.8"/>
|
||||||
|
<path fill="#fff" d="M592.6 181.6s-3.7 1-.2 3.3c3.4 2.3 5 4.2 7.5 4.8 2.5.7 5 1.5 5 4s-.5 3.5-1.9 5.2.8 2.4 2.6 1.5l4.5-2.2c1.1-.7 3-.6 1.4.3-1.9 1-3.7 1.5-1.4 1.5 2.3.1 16.2.4 19.1-.6s6.8-1.2 7-5c0 0 .2-1.6 1.3-2.4 1-.7 1.8-2.3.2-1.2q-2.4 1.8-3 1.3c-.3-.3-.5-.6.7-1.1s1.8 0 2.9-1.5c1-1.6 1-1.4.4-2s-1.8-1-1.2-1.8 1.2-3-1.4-1.7-7.6 4.8-10 5.3c-2.2.5-4 1.2-7.1 1.8s-5 1.4-8.5 3.3c-3.3 1.8-3-1.1-2.5-1.5 0 0 1.3 2.3 4.7-.7 3.4-2.9 2.3-.1 10.6-2.9s6.3-3.1 9.5-4.8 6.4-1.8 4-4c-2.2-2.4-2.4-2.5-5.3 0a35 35 0 0 1-16 6.4s18.8-8 16.9-9.2a21 21 0 0 0-5.3-2.5c-1.3-.3-1.7-.6-4.7.8s-3.5 1.6-4.3 1.7q-1.6-.1-7 2.4c-3.6 1.9-5.5 2.6-8 4 0 0 1.7-3.3 9.1-5.5s11.1-4.2 10.4-4.6-2.7-.8-4-.5c-1.5.3-1-.1-5.6 1.7s-2.6 1.4-6.2 2.2c-3.7.7-5.1 1.5-6.9 2.3 0 0 .8-1 3.1-1.9 1.3-.4-1.3-.9 2.2-1h1a32 32 0 0 0 8.6-3.3c-.7-.1-5.1-.6-9.5 1.6-4.5 2-2.5 1.3-4 1.5-1.6.4-5 2.4-6 3.4-1.1.9-2.7 1.5-2.7 1.5z"/>
|
||||||
|
<path fill="#5a3719" d="M482.7 201.8s1.8.5 2.2 1.8c.5 1.2 1.6-.6 1.6-1.1-.1-.6-1.2-3-3-1.9-2 1.1-1 1.1-.8 1.2"/>
|
||||||
|
<path fill="#7b3c20" d="M477.9 226s3.7-1.8 6.9-1.5c0 0-1.3-4.4.9-3.7 2.1.8 1.5.4 2 .4 0 0 .1-2.9-.5-4 0 0 2.3.5 4.6.5 0 0-2.2-4.1.2-7a7 7 0 0 0 4.2 3.4v-2.3s1.7-.3 3 .4c1.4.8 2.5-7.6-1.5-9.3 0 0-1 1.5-4.7 2.3s-3.7 1.5-5.2 4.3-3 2.9-6.2 5c-3 2-5 6-5 6.4 0 0 1.5 2 1.3 5.1"/>
|
||||||
|
<path fill="#999" d="M603.1 177.8c1.3-.2-1.4-.9 2-1h1a32 32 0 0 0 8.7-3.3c-.7-.1-5.1-.6-9.5 1.5s-2.5 1.4-4 1.7c-1.6.3-5 2.3-6 3.3-1.1.9-2.7 1.5-2.7 1.5s-3.7 1-.2 3.4c3.4 2.3 5 4.2 7.5 4.8 2.5.7 5 1.5 5 4a7 7 0 0 1-1.9 5.2c-1.3 1.7.8 2.4 2.6 1.5l4.5-2.2c1.1-.7 3-.6 1.3.3-1.8 1-3.6 1.5-1.3 1.5 2.3.1 16.2.4 19.1-.6s6.8-1.2 7-5c0 0 .2-1.6 1.3-2.4 1-.7 1.8-2.3.2-1.2-1.5 1.1-2.8 1.7-3 1.3s-.5-.6.7-1.1 1.8 0 2.9-1.5c1-1.6 1-1.4.4-2l-1-.9s-.9-.7-1.9-.1a28 28 0 0 1-7 2.7q-2.2.2-6.5 2.4c-4.3 2.2-8.2 4.6-9 1.7l-2.8 1c-3.4 1.8-3-.7-2.5-1.4 0 0-1.8 2-1.7.2 0-1.8 1.2-1.5 3.3-2.1s5-2 3.8-3-2.7 1-4.2 1.8c-1.4.7-4.3 1.2-4.8-.9s-.4-3.6-4.3-3.8c-4-.2-3.9-2.7-2.8-3.8s2-2.8 5.7-3.5z"/>
|
||||||
|
<path d="M615.6 196.9s6.1-2.8 11.7-4.1c5.7-1.3 1.2.2.3.4-1 .3-9.7 3.2-11.8 4.2-2 1-1.7.2-.2-.4zm1.4 1.3s6.9-2.3 8.2-1.4c1.3 1 .2.6-1.3.8-1.6.1-5.7.8-6.8.8s-.1-.2-.1-.2m11-2.5s1.4-.2 1.5.4c.1.4-.6.5-1.3.4s-1.3-.5-.1-.8z"/>
|
||||||
|
<path fill="#fff" d="M446 255.9s-.3-6.2 2.8-9.2 17.8-18.5 20.1-22.8c0 0 2 1.3 2 3.8 0 0 2.5-4.3 4.5-6 0 0 1.7 1.8 1.5 5.4 0 0 3.5-1.9 9-1.9 0 0-2 2.4-2.1 3.9 0 0 7.6-1 11.7-.3 0 0-10.6 6-7.7 6.5 3.1.5 6.2 0 6.2 0s-3.4 3.3-8.8 4c0 0 6.9 0 8.2 1.5 0 0-6.6 1-12 5 0 0-.4-.2-.4-1.7 0 0-.3 1.4-1.8 2.7-1.5 1.2-5.1 4-6.5 5.3s-3.8 4-6.6 4c0 0 .6-2.1-1.4-2.8a6 6 0 0 0-6 1.5s-7 .1-9.4.5c0 0 1.6-2.6 3-2.6 1.6 0 7.6 1 8.1-3.2.6-4-3.8-3-2.2-5.4 1.7-2.4 1.3-2.2 1.4-2.6 0 0-1.4.8-2.2 3a11 11 0 0 1-4.2 6 15 15 0 0 0-4.8 5s-1.3 0-2.4.4"/>
|
||||||
|
<path fill="#fff" d="M452.8 252.2s.3-.8 2.3-1.2c2.2-.3 2.3-1.3 2-1.8s-1.5-.4.5-2.8c0 0 .8.3 1.2.8.6.6 2.9 5.5-6 5"/>
|
||||||
|
<path fill="#999" d="M447.9 247.9c0 4 5.3 2.5 5.3 2.5a21 21 0 0 0-3.8 3.6c.4-2-3-2.5-3-2.5a12 12 0 0 1 1.5-3.6m19.3-21.5 1.7-2.5s2 1.3 2 3.8c0 0 2.5-4.3 4.5-6 0 0 1.7 1.8 1.5 5.4 0 0 3.5-1.9 9-1.9 0 0-2 2.4-2.1 3.9 0 0 7.6-1 11.7-.3 0 0-10.6 6-7.7 6.5 3.1.5 6.2 0 6.2 0s-3.4 3.3-8.8 4c0 0 6.9 0 8.2 1.5 0 0-2 .3-4.6 1.2 0 0-1.9-1.8-7.6-1.5 0 0 4.4-2.6 8-3.4 0 0-1.6-2-4-.1 0 0-4.9-3.3-.8-6.2 0 0-2.8-.5-4.7.8 0 0 0-2.3 2.1-3.3 0 0-5.4-1-6.7 3 0 0-1-1.5-.5-3.4 0 0-3.3 1.9-4.8 4 0 0-.5-4-2.6-5.5M456.8 252q-1.4.3-4 .2-.2-.1 1.3-1s.4.8 2.7.8"/>
|
||||||
|
<path d="M466.6 236.7s2.5 2 3.3 3c0 0 2.3-1.4 3-2.7 0 0 1.9 1.1 2.4 2.8 0 0 1.3-.8 1.5-2 0 0 3 .6 4.2 1.6 0 0 .4-3 0-4.8 0 0 2.1.2 3.4.7 0 0-1.2-2 5-4.5 0 0-4.7 1.1-6.5 3 0 0-2 .2-2.9-.4v4.5s-1.2-.6-3.5-1.1c0 0-.6 1-1 1.2 0 0-1.5-1.2-2.1-2.7 0 0-2.3 2.1-3 3 0 0-2.3-1.6-3.8-1.6"/>
|
||||||
|
<path fill="#ffc221" d="M452.5 267.3s1 .4 3.3-1.4 8.7-5.9 9.2-9.2c.7-3.3-2-3.4-4-2.5-2.2 1-1.3 2.7-1.2 3.4 0 .6.2 2.9-3.3 6z"/>
|
||||||
|
<path fill="#ffc221" d="M451.9 268.3s-5.2-2.2-.6-4.5 6.7-2.9 7.2-4.9c.6-1.9.2-1.5-1.5-.7-1.7.7-8.2 3.8-9.2 1 0 0 2.7 1 6-.6 3.4-1.7 6.2-2.1 4-2.8a37 37 0 0 0-11 .5c-1.4.4-1 .3-1.3 1.6-.2 1.3-1.6 4-2.2 4.7-.5.8-1.8 4 .6 5.5a9 9 0 0 0 8 .2"/>
|
||||||
|
<path d="M449.9 257s-1.3.2-1 .7q.3.6 1 .4c.3 0 1-.2 1.1-.5 0-.3-.8-.7-1.1-.5z"/>
|
||||||
|
<path fill="#fff" d="M451.5 267.1s-2.4-1.1.4-2.6 5.6-3 6-3.6c0 0-1.3 1.9-6.4 6.2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" id="flag-icons-asean" viewBox="0 0 640 480">
|
||||||
|
<path fill="#0039a6" d="M0 0h640v480H0z"/>
|
||||||
|
<circle cx="320" cy="240" r="144" fill="#fff"/>
|
||||||
|
<circle cx="320" cy="240" r="137.3" fill="#ed2939"/>
|
||||||
|
<use xlink:href="#asean-a" transform="matrix(-1 0 0 1 640 0)"/>
|
||||||
|
<g id="asean-a" fill="#f9e300">
|
||||||
|
<path d="M357 240c24-14.4 35-43.2 35-72h-11v1c0 9.6-1.5 44.6-27.9 71a106 106 0 0 1 27.9 71v1h11c0-28.8-11.5-57.6-35-72"/>
|
||||||
|
<path d="M377.6 169v-1h-11.5v1.4c0 9.6-2 43.2-20.7 70.6 19.2 27.4 20.7 61 20.7 70.6v1.4h11.5v-1c0-9.6-2.4-44.6-27.8-71a106 106 0 0 0 27.8-71"/>
|
||||||
|
<path d="m341.1 240 1-1a130 130 0 0 0 20.1-69.6V168h-10.5v2c0 10-1.5 42.2-14.4 70a182 182 0 0 1 14.4 70v2h10.5v-1.4c0-9.6-1-39.9-20.1-69.6"/>
|
||||||
|
<path d="M333.4 240a178 178 0 0 0 14.4-72h-11v3.4c0 12-1 41.2-7.2 68.6a336 336 0 0 1 7.2 68.6v3.4h10.6v-2c0-10-1-43.1-13.5-69.5"/>
|
||||||
|
<path d="M325.8 240a331 331 0 0 0 6.7-68.6V168h-10.6v144h10.6v-3.4c0-11.5 0-41.2-6.7-68.1"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-at" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fff" d="M0 160h640v160H0z"/>
|
||||||
|
<path fill="#c8102e" d="M0 0h640v160H0zm0 320h640v160H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 195 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-au" viewBox="0 0 640 480">
|
||||||
|
<path fill="#00008B" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fff" d="m37.5 0 122 90.5L281 0h39v31l-120 89.5 120 89V240h-40l-120-89.5L40.5 240H0v-30l119.5-89L0 32V0z"/>
|
||||||
|
<path fill="red" d="M212 140.5 320 220v20l-135.5-99.5zm-92 10 3 17.5-96 72H0zM320 0v1.5l-124.5 94 1-22L295 0zM0 0l119.5 88h-30L0 21z"/>
|
||||||
|
<path fill="#fff" d="M120.5 0v240h80V0zM0 80v80h320V80z"/>
|
||||||
|
<path fill="red" d="M0 96.5v48h320v-48zM136.5 0v240h48V0z"/>
|
||||||
|
<path fill="#fff" d="m527 396.7-20.5 2.6 2.2 20.5-14.8-14.4-14.7 14.5 2-20.5-20.5-2.4 17.3-11.2-10.9-17.5 19.6 6.5 6.9-19.5 7.1 19.4 19.5-6.7-10.7 17.6zm-3.7-117.2 2.7-13-9.8-9 13.2-1.5 5.5-12.1 5.5 12.1 13.2 1.5-9.8 9 2.7 13-11.6-6.6zm-104.1-60-20.3 2.2 1.8 20.3-14.4-14.5-14.8 14.1 2.4-20.3-20.2-2.7 17.3-10.8-10.5-17.5 19.3 6.8L387 178l6.7 19.3 19.4-6.3-10.9 17.3 17.1 11.2ZM623 186.7l-20.9 2.7 2.3 20.9-15.1-14.7-15 14.8 2.1-21-20.9-2.4 17.7-11.5-11.1-17.9 20 6.7 7-19.8 7.2 19.8 19.9-6.9-11 18zm-96.1-83.5-20.7 2.3 1.9 20.8-14.7-14.8-15.1 14.4 2.4-20.7-20.7-2.8 17.7-11L467 73.5l19.7 6.9 7.3-19.5 6.8 19.7 19.8-6.5-11.1 17.6zM234 385.7l-45.8 5.4 4.6 45.9-32.8-32.4-33 32.2 4.9-45.9-45.8-5.8 38.9-24.8-24-39.4 43.6 15 15.8-43.4 15.5 43.5 43.7-14.7-24.3 39.2 38.8 25.1Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,186 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-aw" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="aw-a">
|
||||||
|
<path fill-opacity=".7" d="M0 0h288v216H0z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clip-path="url(#aw-a)" transform="scale(2.2222)">
|
||||||
|
<path fill="#39c" d="M0 0v216h324V0z"/>
|
||||||
|
<path fill="#ff0" d="M0 144v12h324v-12zm0 24v12h324v-12z"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#9cc" d="m142.7 28 2.9 3zm-3 6 3 3zm5.9 0 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m139.7 37 3 2.9-3-3m5.9 0 3 3z"/>
|
||||||
|
<path fill="#6cc" d="m136.7 42.8 3 3z"/>
|
||||||
|
<path fill="#c66" d="m142.7 42.8 2.9 3z"/>
|
||||||
|
<path fill="#6cc" d="m148.6 42.8 2.9 3z"/>
|
||||||
|
<path fill="#ccf" d="m136.7 45.8 3 3zm11.9 0 2.9 3z"/>
|
||||||
|
<path fill="#fcc" d="m139.7 48.7 3 3zm5.9 0 3 3z"/>
|
||||||
|
<path fill="#6cc" d="m133.8 51.7 3 3z"/>
|
||||||
|
<path fill="#c00" stroke="#fff" stroke-width="3.7" d="m142.2 34-20.7 78.5L42.8 134l78.4 20.5 21 78.4 20.9-78.4 78.4-21-78.4-20.9-21-78.4z"/>
|
||||||
|
<path fill="#6cc" d="m151.5 51.7 3 3z"/>
|
||||||
|
<path fill="#9cf" d="m133.8 54.6 3 3zm17.7 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m136.7 57.6 3 3zm11.9 0 2.9 3z"/>
|
||||||
|
<path fill="#69c" d="m130.8 60.5 3 3z"/>
|
||||||
|
<path fill="#c33" d="m137.7 62.5 1 2zm11.8 0 1 2z"/>
|
||||||
|
<path fill="#69c" d="m154.5 60.5 3 3z"/>
|
||||||
|
<path fill="#9cf" d="m130.8 63.5 3 3zm23.7 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m133.8 66.4 3 3zm17.7 0 3 3z"/>
|
||||||
|
<path fill="#69c" d="m127.9 69.4 3 3zm29.5 0 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m127.9 72.3 3 3zm29.5 0 3 3z"/>
|
||||||
|
<path fill="#cff" d="m127.9 75.3 3 3zm29.5 0 3 3z"/>
|
||||||
|
<path fill="#69c" d="m125 78.3 2.9 2.9z"/>
|
||||||
|
<path fill="#fcc" d="m130.8 78.3 3 2.9zm23.7 0 3 3z"/>
|
||||||
|
<path fill="#69c" d="m160.4 78.3 3 2.9z"/>
|
||||||
|
<path fill="#9cc" d="m125 81.2 2.9 3z"/>
|
||||||
|
<path fill="#c33" d="m131.8 83.2 1 2zm23.6 0 1 2z"/>
|
||||||
|
<path fill="#9cc" d="m160.4 81.2 3 3z"/>
|
||||||
|
<path fill="#cff" d="m125 84.2 2.9 3zm35.5 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m127.9 87.1 3 3zm29.5 0 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m122 90 3 3z"/>
|
||||||
|
<path fill="#c33" d="m128.9 92 1 2zm29.5 0 1 2z"/>
|
||||||
|
<path fill="#9cc" d="m163.3 90 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m122 93 3 3zm41.3 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m125 96 2.9 3zm35.5 0 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m119 99 3 2.9z"/>
|
||||||
|
<path fill="#c33" d="m126 100.9.9 2zm35.4 0 1 2z"/>
|
||||||
|
<path fill="#9cc" d="m166.3 99 3 2.9z"/>
|
||||||
|
<path fill="#ccf" d="m119 101.9 3 3zm47.3 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m122 104.8 3 3zm41.3 0 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m116 107.8 3 3z"/>
|
||||||
|
<path fill="#c33" d="m122 107.8 3 3zm41.3 0 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m169.2 107.8 3 3zm-62 3 3 2.9z"/>
|
||||||
|
<path fill="#ccf" d="m110.2 110.7 3 3zm65 0 2.9 3z"/>
|
||||||
|
<path fill="#9cc" d="m178 110.7 3 3zm-79.6 3 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m101.3 113.7 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m113.1 113.7 3 3z"/>
|
||||||
|
<path fill="#c33" d="m116 113.7 3 3zm53.2 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m172.2 113.7 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m184 113.7 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m187 113.7 2.9 3z"/>
|
||||||
|
<path fill="#69c" d="m86.6 116.6 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m89.5 116.6 3 3z"/>
|
||||||
|
<path fill="#cff" d="m92.5 116.6 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m104.3 116.6 3 3z"/>
|
||||||
|
<path fill="#c33" d="m109.2 117.6 2 1zm67.9 0 2 1z"/>
|
||||||
|
<path fill="#fcc" d="m181 116.6 3 3z"/>
|
||||||
|
<path fill="#cff" d="m192.8 116.6 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m195.8 116.6 3 3z"/>
|
||||||
|
<path fill="#69c" d="m198.7 116.6 3 3zm-121 3 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m80.7 119.6 3 3z"/>
|
||||||
|
<path fill="#cff" d="m83.6 119.6 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m95.4 119.6 3 3z"/>
|
||||||
|
<path fill="#c33" d="m100.3 120.6 2 1zm85.6 0 2 1z"/>
|
||||||
|
<path fill="#fcc" d="m189.9 119.6 3 3z"/>
|
||||||
|
<path fill="#cff" d="m201.7 119.6 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m204.6 119.6 3 3z"/>
|
||||||
|
<path fill="#69c" d="m207.6 119.6 3 3zm-138.8 3 3 2.9z"/>
|
||||||
|
<path fill="#9cf" d="m71.8 122.5 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m86.6 122.5 3 3z"/>
|
||||||
|
<path fill="#c33" d="m91.5 123.5 2 1zm103.3 0 2 1z"/>
|
||||||
|
<path fill="#fcc" d="m198.7 122.5 3 3z"/>
|
||||||
|
<path fill="#9cf" d="m213.5 122.5 3 3z"/>
|
||||||
|
<path fill="#69c" d="m216.4 122.5 3 3z"/>
|
||||||
|
<path fill="#6cc" d="m60 125.5 3 3z"/>
|
||||||
|
<path fill="#9cf" d="m63 125.5 2.9 3z"/>
|
||||||
|
<path fill="#fcc" d="m74.8 125.5 2.9 3zm135.8 0 2.9 3z"/>
|
||||||
|
<path fill="#9cf" d="m222.3 125.5 3 3z"/>
|
||||||
|
<path fill="#6cc" d="m225.3 125.5 3 3zm-174.2 3 3 2.9z"/>
|
||||||
|
<path fill="#ccf" d="m54 128.4 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m65.9 128.4 3 3z"/>
|
||||||
|
<path fill="#c33" d="m70.8 129.4 2 1zm144.7 0 2 1z"/>
|
||||||
|
<path fill="#fcc" d="m219.4 128.4 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m231.2 128.4 3 3z"/>
|
||||||
|
<path fill="#6cc" d="m234.2 128.4 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m42.3 131.4 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m45.2 131.4 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m57 131.4 3 3zm171.3 0 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m240 131.4 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m243 131.4 3 3zm-206.6 3 3 2.9z"/>
|
||||||
|
<path fill="#c66" d="m51.1 134.3 3 3zm183 0 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m249 134.3 2.9 3zm-206.6 3 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m45.2 137.3 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m57 137.3 3 3zm171.3 0 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m240 137.3 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m243 137.3 3 3z"/>
|
||||||
|
<path fill="#6cc" d="m51.1 140.3 3 2.9z"/>
|
||||||
|
<path fill="#ccf" d="m54 140.3 3 2.9z"/>
|
||||||
|
<path fill="#fcc" d="m65.9 140.3 3 2.9z"/>
|
||||||
|
<path fill="#c33" d="m70.8 141.2 2 1zm144.7 0 2 1z"/>
|
||||||
|
<path fill="#fcc" d="m219.4 140.3 3 2.9z"/>
|
||||||
|
<path fill="#ccf" d="m231.2 140.3 3 2.9z"/>
|
||||||
|
<path fill="#6cc" d="m234.2 140.3 3 2.9zm-174.2 3 3 3z"/>
|
||||||
|
<path fill="#9cf" d="m63 143.2 2.9 3z"/>
|
||||||
|
<path fill="#fcc" d="m74.8 143.2 2.9 3zm135.8 0 2.9 3z"/>
|
||||||
|
<path fill="#9cf" d="m222.3 143.2 3 3z"/>
|
||||||
|
<path fill="#6cc" d="m225.3 143.2 3 3z"/>
|
||||||
|
<path fill="#69c" d="m68.8 146.2 3 2.9z"/>
|
||||||
|
<path fill="#9cf" d="m71.8 146.2 3 2.9z"/>
|
||||||
|
<path fill="#fcc" d="m86.6 146.2 3 2.9z"/>
|
||||||
|
<path fill="#c33" d="m91.5 147.1 2 1zm103.3 0 2 1z"/>
|
||||||
|
<path fill="#fcc" d="m198.7 146.2 3 2.9z"/>
|
||||||
|
<path fill="#9cf" d="m213.5 146.2 3 2.9z"/>
|
||||||
|
<path fill="#69c" d="m216.4 146.2 3 2.9zm-138.7 3 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m80.7 149.1 3 3z"/>
|
||||||
|
<path fill="#cff" d="m83.6 149.1 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m95.4 149.1 3 3z"/>
|
||||||
|
<path fill="#c33" d="m100.3 150 2 1zm85.6 0 2 1z"/>
|
||||||
|
<path fill="#fcc" d="m189.9 149.1 3 3z"/>
|
||||||
|
<path fill="#cff" d="m201.7 149.1 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m204.6 149.1 3 3z"/>
|
||||||
|
<path fill="#69c" d="m207.6 149.1 3 3zm-121 3 2.9 2.9z"/>
|
||||||
|
<path fill="#9cc" d="m89.5 152 3 3z"/>
|
||||||
|
<path fill="#cff" d="m92.5 152 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m104.3 152 3 3z"/>
|
||||||
|
<path fill="#c33" d="m109.2 153 2 1zm67.9 0 2 1z"/>
|
||||||
|
<path fill="#fcc" d="m181 152 3 3z"/>
|
||||||
|
<path fill="#cff" d="m192.8 152 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m195.8 152 3 3z"/>
|
||||||
|
<path fill="#69c" d="m198.7 152 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m98.4 155 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m101.3 155 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m113.1 155 3 3z"/>
|
||||||
|
<path fill="#c33" d="m116 155 3 3zm53.2 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m172.2 155 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m184 155 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m187 155 2.9 3zm-79.7 3 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m110.2 158 3 3zm65 0 2.9 3z"/>
|
||||||
|
<path fill="#9cc" d="m178 158 3 3zm-62 3 3 2.9z"/>
|
||||||
|
<path fill="#c33" d="m122 161 3 2.9zm41.3 0 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m169.2 161 3 2.9z"/>
|
||||||
|
<path fill="#fcc" d="m122 163.9 3 3zm41.3 0 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m119 166.8 3 3z"/>
|
||||||
|
<path fill="#c33" d="m126 168.8.9 2zm35.4 0 1 2z"/>
|
||||||
|
<path fill="#ccf" d="m166.3 166.8 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m119 169.8 3 3zm47.3 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m125 172.7 2.9 3zm35.5 0 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m122 175.7 3 3z"/>
|
||||||
|
<path fill="#c33" d="m128.9 177.6 1 2zm29.5 0 1 2z"/>
|
||||||
|
<path fill="#ccf" d="m163.3 175.7 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m122 178.6 3 3zm41.3 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m127.9 181.6 3 3zm29.5 0 3 3z"/>
|
||||||
|
<path fill="#cff" d="m125 184.5 2.9 3z"/>
|
||||||
|
<path fill="#c33" d="m131.8 186.5 1 2zm23.6 0 1 2z"/>
|
||||||
|
<path fill="#cff" d="m160.4 184.5 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m125 187.5 2.9 3zm35.5 0 3 3z"/>
|
||||||
|
<path fill="#69c" d="m125 190.4 2.9 3z"/>
|
||||||
|
<path fill="#fcc" d="m130.8 190.4 3 3zm23.7 0 3 3z"/>
|
||||||
|
<path fill="#69c" d="m160.4 190.4 3 3z"/>
|
||||||
|
<path fill="#cff" d="m127.9 193.4 3 3zm29.5 0 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m127.9 196.3 3 3zm29.5 0 3 3z"/>
|
||||||
|
<path fill="#69c" d="m127.9 199.3 3 3zm29.5 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m133.8 202.2 3 3zm17.7 0 3 3z"/>
|
||||||
|
<path fill="#9cf" d="m130.8 205.2 3 3z"/>
|
||||||
|
<path fill="#c33" d="m137.7 207.2 1 2zm11.8 0 1 2z"/>
|
||||||
|
<path fill="#9cf" d="m154.5 205.2 3 3z"/>
|
||||||
|
<path fill="#69c" d="m130.8 208.2 3 2.9zm23.7 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m136.7 211.1 3 3zm11.9 0 2.9 3z"/>
|
||||||
|
<path fill="#9cf" d="m133.8 214 3 3zm17.7 0 3 3z"/>
|
||||||
|
<path fill="#6cc" d="m133.8 217 3 3zm17.7 0 3 3z"/>
|
||||||
|
<path fill="#fcc" d="m139.7 220 3 3zm5.9 0 3 3z"/>
|
||||||
|
<path fill="#ccf" d="m136.7 222.9 3 3zm11.9 0 2.9 3z"/>
|
||||||
|
<path fill="#6cc" d="m136.7 225.9 3 3z"/>
|
||||||
|
<path fill="#c66" d="m142.7 225.9 2.9 3z"/>
|
||||||
|
<path fill="#6cc" d="m148.6 225.9 2.9 3z"/>
|
||||||
|
<path fill="#ccf" d="m139.7 231.8 3 3zm5.9 0 3 3z"/>
|
||||||
|
<path fill="#9cc" d="m139.7 234.7 3 3zm5.9 0 3 3zm-3 6 3 2.9z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ax" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="ax-a">
|
||||||
|
<path fill-opacity=".7" d="M106.3 0h1133.3v850H106.3z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clip-path="url(#ax-a)" transform="matrix(.56472 0 0 .56482 -60 -.1)">
|
||||||
|
<path fill="#0053a5" d="M0 0h1300v850H0z"/>
|
||||||
|
<g fill="#ffce00">
|
||||||
|
<path d="M400 0h250v850H400z"/>
|
||||||
|
<path d="M0 300h1300v250H0z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#d21034">
|
||||||
|
<path d="M475 0h100v850H475z"/>
|
||||||
|
<path d="M0 375h1300v100H0z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 556 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-az" viewBox="0 0 640 480">
|
||||||
|
<path fill="#3f9c35" d="M.1 0h640v480H.1z"/>
|
||||||
|
<path fill="#ed2939" d="M.1 0h640v320H.1z"/>
|
||||||
|
<path fill="#00b9e4" d="M.1 0h640v160H.1z"/>
|
||||||
|
<circle cx="304" cy="240" r="72" fill="#fff"/>
|
||||||
|
<circle cx="320" cy="240" r="60" fill="#ed2939"/>
|
||||||
|
<path fill="#fff" d="m384 200 7.7 21.5 20.6-9.8-9.8 20.7L424 240l-21.5 7.7 9.8 20.6-20.6-9.8L384 280l-7.7-21.5-20.6 9.8 9.8-20.6L344 240l21.5-7.7-9.8-20.6 20.6 9.8z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 501 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ba" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="ba-a">
|
||||||
|
<path fill-opacity=".7" d="M-85.3 0h682.6v512H-85.3z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g fill-rule="evenodd" clip-path="url(#ba-a)" transform="translate(80)scale(.9375)">
|
||||||
|
<path fill="#009" d="M-85.3 0h682.6v512H-85.3z"/>
|
||||||
|
<path fill="#FC0" d="m56.5 0 511 512.3V.3z"/>
|
||||||
|
<path fill="#FFF" d="M439.9 481.5 412 461.2l-28.6 20.2 10.8-33.2-28.2-20.5h35l10.8-33.2 10.7 33.3h35l-28 20.7zm81.3 10.4-35-.1-10.7-33.3-10.8 33.2h-35l28.2 20.5-10.8 33.2 28.6-20.2 28 20.3-10.5-33zM365.6 384.7l28-20.7-35-.1-10.7-33.2-10.8 33.2-35-.1 28.2 20.5-10.8 33.3 28.6-20.3 28 20.4zm-64.3-64.5 28-20.6-35-.1-10.7-33.3-10.9 33.2h-34.9l28.2 20.5-10.8 33.2 28.6-20.2 27.9 20.3zm-63.7-63.6 28-20.7h-35L220 202.5l-10.8 33.2h-35l28.2 20.4-10.8 33.3 28.6-20.3 28 20.4-10.5-33zm-64.4-64.3 28-20.6-35-.1-10.7-33.3-10.9 33.2h-34.9L138 192l-10.8 33.2 28.6-20.2 27.9 20.3-10.4-33zm-63.6-63.9 27.9-20.7h-35L91.9 74.3 81 107.6H46L74.4 128l-10.9 33.2L92.1 141l27.8 20.4zm-64-64 27.9-20.7h-35L27.9 10.3 17 43.6h-35L10.4 64l-11 33.3L28.1 77l27.8 20.4zm-64-64L9.4-20.3h-35l-10.7-33.3L-47-20.4h-35L-53.7 0l-10.8 33.2L-35.9 13l27.8 20.4z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-bb" viewBox="0 0 640 480">
|
||||||
|
<path fill="#00267f" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#ffc726" d="M213.3 0h213.4v480H213.3z"/>
|
||||||
|
<path id="bb-a" fill="#000001" d="M319.8 135.5c-7 19-14 38.6-29.2 53.7 4.7-1.6 13-3 18.2-2.8v79.5l-22.4 3.3c-.8 0-1-1.3-1-3-2.2-24.7-8-45.5-14.8-67-.5-2.9-9-14-2.4-12 .8 0 9.5 3.6 8.2 1.9a85 85 0 0 0-46.4-24c-1.5-.3-2.4.5-1 2.2 22.4 34.6 41.3 75.5 41.1 124 8.8 0 30-5.2 38.7-5.2v56.1H320l2.5-156.7z"/>
|
||||||
|
<use xlink:href="#bb-a" width="100%" height="100%" transform="matrix(-1 0 0 1 639.5 0)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bd" viewBox="0 0 640 480">
|
||||||
|
<path fill="#006a4e" d="M0 0h640v480H0z"/>
|
||||||
|
<circle cx="280" cy="240" r="160" fill="#f42a41"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 187 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-be" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd" stroke-width="1pt">
|
||||||
|
<path fill="#000001" d="M0 0h213.3v480H0z"/>
|
||||||
|
<path fill="#ffd90c" d="M213.3 0h213.4v480H213.3z"/>
|
||||||
|
<path fill="#f31830" d="M426.7 0H640v480H426.7z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 302 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bf" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd">
|
||||||
|
<path fill="#de0000" d="M640 479.6H.4V0H640z"/>
|
||||||
|
<path fill="#35a100" d="M639.6 480H0V240.2h639.6z"/>
|
||||||
|
<path fill="#fff300" d="m254.6 276.2-106-72.4h131L320 86.6 360.4 204l131-.1-106 72.4 40.5 117.3-106-72.6L214 393.4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 353 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bg" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fff" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="#00966e" d="M0 160h640v160H0z"/>
|
||||||
|
<path fill="#d62612" d="M0 320h640v160H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bh" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fff" d="M0 0h640v480H0"/>
|
||||||
|
<path fill="#ce1126" d="M640 0H96l110.7 48L96 96l110.7 48L96 192l110.7 48L96 288l110.7 48L96 384l110.7 48L96 480h544"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bi" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="bi-a">
|
||||||
|
<path fill-opacity=".7" d="M-90.5 0H592v512H-90.5z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g fill-rule="evenodd" clip-path="url(#bi-a)" transform="translate(84.9)scale(.9375)">
|
||||||
|
<path fill="#18b637" d="m-178 0 428.8 256L-178 512zm857.6 0L250.8 256l428.8 256z"/>
|
||||||
|
<path fill="#cf0921" d="m-178 0 428.8 256L679.6 0zm0 512 428.8-256 428.8 256z"/>
|
||||||
|
<path fill="#fff" d="M679.6 0h-79.9L-178 464.3V512h79.9L679.6 47.7z"/>
|
||||||
|
<path fill="#fff" d="M398.9 256a148 148 0 1 1-296.1 0 148 148 0 0 1 296 0z"/>
|
||||||
|
<path fill="#fff" d="M-178 0v47.7L599.7 512h79.9v-47.7L-98.1 0z"/>
|
||||||
|
<path fill="#cf0921" stroke="#18b637" stroke-width="3.9" d="m280 200.2-19.3.3-10 16.4-9.9-16.4-19.2-.4 9.3-16.9-9.2-16.8 19.2-.4 10-16.4 9.9 16.5 19.2.4-9.3 16.8zm-64.6 111.6-19.2.3-10 16.4-9.9-16.4-19.2-.4 9.3-16.9-9.2-16.8 19.2-.4 10-16.4 9.9 16.5 19.2.4-9.3 16.8zm130.6 0-19.2.3-10 16.4-10-16.4-19.1-.4 9.3-16.9-9.3-16.8 19.2-.4 10-16.4 10 16.5 19.2.4-9.4 16.8z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bj" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="bj-a">
|
||||||
|
<path fill="gray" d="M67.6-154h666v666h-666z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clip-path="url(#bj-a)" transform="matrix(.961 0 0 .7207 -65 111)">
|
||||||
|
<g fill-rule="evenodd" stroke-width="1pt">
|
||||||
|
<path fill="#319400" d="M0-154h333v666H0z"/>
|
||||||
|
<path fill="#ffd600" d="M333-154h666v333H333z"/>
|
||||||
|
<path fill="#de2110" d="M333 179h666v333H333z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 499 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bl" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fff" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#000091" d="M0 0h213.3v480H0z"/>
|
||||||
|
<path fill="#e1000f" d="M426.7 0H640v480H426.7z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 231 B |
@@ -0,0 +1,97 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bm" viewBox="0 0 640 480">
|
||||||
|
<path fill="#cf142b" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fff" d="M559.7 130.6v161.6c0 43.1-86.2 57.2-86.2 57.2s-86.4-14-86.4-57.4V130.6h172.5z"/>
|
||||||
|
<path fill="#2f8f22" d="M559.7 292.2c0 43.1-86.2 57.2-86.2 57.2s-86.4-14-86.4-57.4c0 0 0-3.5 1.8-5.4 0 0-1 7.1 4.5 12.6 0 0-4.3-7.8 0-15.3 0 0-1.7 9.8 4.4 15.3 0 0-3.3-7.9.4-16.7 0 0-1.8 14.3 4.7 17.3 0 0 1.8-8.4-.8-13.6 0 0 4.5 1.8 4.3 13.8 0 0 1.4-1.8 1.8-10.5 0 0 .2 10 3.5 12.3 0 0 1.2-1-.3-5.5-1.6-4.4.6-6 1-6 0 0-.8 5 3.4 8.8 0 0-1.8-7.9.7-9 0 0-.6 6.7 4.8 8.1 0 0 .3-1.9-.8-4 0 0-1-2.5-.3-4.5 0 0 1.7 6 4 7 0 0-1.4-3.6 0-7 0 0 .2 5 4.7 7.1 0 0-3-4-1.9-8.2l28.7 1.4 15 .7 44.7-3 7.6-6.7s3.1 4.1-1.8 10.8c0 0 4.8-.8 6.3-8.3 0 0 2 4.1-.7 8.8 0 0 5.3-5.4 6-11.3 0 0 2.1 5.8-2.9 12 0 0 4.4-1.6 6.3-8.1 0 0 1.6 4-2.7 9.5 0 0 8.1-4.1 7.9-13 0 0 3.3 4.8-.5 11.6 0 0 4-3.7 4.5-9.3 0 0 2.3 2.5-.2 9.4 0 0 5-4.8 5.8-9.9 0 0 1 4.8-3.2 10.8 0 0 2.9-.8 5.7-6.6 0 0 .7 2.4-1.8 6.6 0 0 2.8-.5 4.6-5.9 0 0 .3 3.2-.5 6 0 0 2.1-1.3 2.6-7.3a9 9 0 0 0 1.2 4.4v.8z"/>
|
||||||
|
<path fill="#d40000" stroke="#000" stroke-linejoin="round" stroke-width="1.2" d="M515.4 198.7s-3 .6-7-.6c-3.8-1.2-5.3-.7-6.5 0 0 0 1.5-3.1-2.3-5.6 0 0 1.2 3.2-.5 4.6 0 0-.7.7-1.6-.3 0 0-1.3-1.5-2.8-2.4 0 0 3.3-1.2 2.7-4.5s-2.4-3.6-3.3-4.1a6 6 0 0 0 0 2.4s-3.5-2 1.2-4.7 4-4.4 3.1-5.8a13 13 0 0 0-3.5-3.6s1 1.6.7 2.9c-.1 1.2-2.4 2-2-.1.5-2.4 0-2 0-4.3 0 0 4.2 1.4 6-2.8 0 0 1.6-4.3-3.8-6.4 0 0 1.3 1.8.7 3 0 0-1.2 2.2-2.5.6s-2.2-2.2-2.1-4.2c0 0 4.9.7 3.6-4.7 0 0-.9 3.6-7.2-1.2 0 0 4.2-4.2 2.5-7.6 0 0-.5-1.5-4.9-.7 0 0 3.8-2.4 2.3-4.5 0 0-.9-1.3-4.6.4 0 0 1.5-2.3-2.1-5 0 0-2.4 1.2-3.6 2.4 0 0-2.4-3-4-4.3 0 0-2.8 1.1-3.5 4.3 0 0-1.3-1.5-4.3-2.4 0 0-1.4 2.8.5 5 0 0-1.4 0-4-1.1 0 0-2.8-1.2-2.3 1.1.3 2.4.6 3 1.2 4.2 0 0-6.5-1.5-6.2 2a9 9 0 0 0 3 6.2s-3.5 4.8-6.6 1.2c0 0-1.2 1.2 1.2 4.2 0 0 2.4 2.5.3 4.2 0 0-2.5 2-3.7-2 0 0-4.1 4.1.8 7.2 0 0 3 1.8 6.3-1 0 0-1 8-4 6.4 0 0-1.9-1.2 1.4-2.9 0 0-4.8-.6-5.5 4 0 0-.6 3.5 3.5 5 0 0 3.1 1.2 0 3.6 0 0-2.5 1.7-.9 4.4 0 0 1.8 2.8-2.9 3.2 0 0-2.4 0-3.3-.4 0 0-1 2-.4 4 0 0-2.3-1.7-7.5.2-5.2 2-4.9.6-5.2 1.2l-1.6 2.4s2.8 3.6 2.9 3.3l-.6 4 1.4.6 10.8-4.8 11-5.7 8.9.4 5.6 1.3 7.2.6 5.4-2.8h7.6l8.4 4.2 9.5 5.7 5 1 3.9-.3v-7.8z"/>
|
||||||
|
<path fill="#d40000" stroke="#000" stroke-linejoin="round" stroke-width="1.2" d="M428.2 207.7s4.2 2.2 6-.3c0 0 2.2-4.4-2.8-6 0 0 2.8-3.2-.2-6.2 0 0-1.6-1.6-4.2-.5 0 0-1.2-2.3-3.7-2.2 0 0-2.4 0-3 2.5 0 0-3.2-1.2-4.9.6 0 0-3 3.2 1.1 5.8l2.9.3 2.8-1.4 3.3.9s-.9 3.2 2.7 6.5z"/>
|
||||||
|
<path fill="#64b4d1" stroke="#000" stroke-width="1.2" d="M521.6 200.1a7 7 0 0 1 6.9 3.8c2.6 6.2-3 9.6-3 9.6.4 1.3.5 3.2.5 3.2 7.8 1 6.4 9.8 6.4 9.8l-2.6-2.2c-4.5-1.8-9.3 2.2-12.6 8.6-3.4 6.6-1.8 9.5-1.2 17.3.6 7.7 13 12.4 13 12.4l-9.7 25c-3.8 10-12 5.9-14.3 3.7-2.3-2-2.9-.8-4 0-1.1 1 5.3 6-6.5 10.8-11.7 4.7-13.7 8.3-15.7 9.5s-10 .5-10.8-.6c-1-1-.4-1-3.5-2.9-3-1.7-8.2-3.5-13.8-6.2s-5.4-6.2-5.3-6.9c0-.7 1.9-6.5-4.7-1.9-6.5 4.7-12.1-2.2-12.1-2.2-1.2-1.7-6.8-16.4-6.8-16.4a92 92 0 0 0-4.3-12s-.4.8 4.5-2c4.8-2.7 7-7.3 8.7-11.9s0-12.5-.6-14c-.6-1.6-4.2-8.9-8.7-10.4-4.4-1.6-7.5 2.4-7.5 2.4s-1.3-9 6.6-10c0 0 0-1.8.4-3.1 0 0-5.6-3.4-3-9.6 0 0 1.8-4.3 6.8-3.8l-1.1 2.4s-1.2 12.6 17.2 4.2c18.5-8.6 18-10.2 28.7-4.8l7.6-.1s11-5.3 14.7-3c3.5 2.3 16.3 9.4 16.3 9.4s12.4 4.7 14.9-4z"/>
|
||||||
|
<path fill="#fff" stroke="#00247d" stroke-miterlimit="10" stroke-width="1.2" d="M465.8 255s-.6-3.9-1.2-6.4c0 0-1.5-3.9 1-6.8l2.8-3.3s1.8-2.4 4-2.7c0 0 2.3 0 2.4-.5.3-.5 2.8-4.5 8.6 0 0 0 1.8-3 4.8-3.6 0 0 3-.8 4.5 1.4 0 0 3.5-2.6 6.4 1.7 0 0 4.1-2.4 7.2 2.3 0 0 4-2 6.4 2.1 2.5 4.3 2 6 2 6l2 6.8 6.5 8-15.2 5.8h-7l-13.7 3.6-24.3 1.9-6.7-8 9.6-8.3z"/>
|
||||||
|
<path fill="#d40000" stroke="#000" stroke-linejoin="round" stroke-width="1.2" d="M445.3 295.5s-3.6.1-5.5 1.2c-1.8.8-3.3 1.8-5.4 3.2 0 0-1 1.3-5.2.5 0 0-7.2-1.7-7.2 4 0 0-8.8.6-5.2 8.3 0 0 2.3 6 7.3 1.8 0 0-3.2 4.5 3 6.3 0 0 4.4 1.2 5.8-3.5 0 0 .7-1.8-1-4 0 0 2.1-.4 3.6-2.7 0 0-4.6 5.8.5 8 0 0 6.3 1.5 6.6-5 0 0-.6-3.2 2-4.3 0 0 5-1.2 7-6.6 0 0-7-3.9-6.3-7.2zM430.6 238s-5.6-2.5-8.4 0c0 0-3.5-2.2-7.5 0 0 0-3.7 2.4-6.3 5.2 0 0-1.8 1.5-1.2 6.4 0 0 1 3.5.5 5.1 0 0-1.2-.2-3.7 2.8 0 0-3.1 3.5-6 .3 0 0 1 4.6 6.1 3.8 0 0-2.5 2-.3 7 0 0 1.6 3.6-1 8.2 0 0 4.5-1.8 4.4-7.2 0 0-.5-3.6 1-6 0 0-1.5 2.2 1.6 7.1 0 0 2.4 3.6.5 7.2 0 0 4.4-1.6 4-7s-3-3.2-1.4-8.2c0 0 .5 2.6 1.8 4a7 7 0 0 1 2.4 7s2.6-3.4 2-6.8a13 13 0 0 0-1.3-4.4l8.4-4.8 4.3-7.5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="M417.8 259.5s-3.5-.2-4-3m-6.1-1.8s1.2 0 2.4 1.2c0 0 .7 1.2 1.9 1"/>
|
||||||
|
<path d="M423 249.2s-1.8 0-3-.8c0 0-.9-.7-1.5.4 0 0-1 1.6.8 2.3 0 0 2.2 1.2-1.2 3.4 0 0 4-1.6 2.4-3.6 0 0-1.8-1.2-1.2-1.7 0 0 .2-.4 1 0 .6.6 2 .4 2.7 0"/>
|
||||||
|
<path fill="#784421" stroke="#000" stroke-width="1.2" d="m477.6 259 2.1-.6-14-48h-.4z"/>
|
||||||
|
<path d="M431.2 312.3s-3.2-1.9-6 .3c0 0 .4-1.2 2.4-1.7 0 0 1.2-3.2 4.3-2.9 0 0-1.3 1.6-3 2.8 0 0 1.8.2 2.4 1.5z"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M419.7 319.8s-5.2-3.2-2.3-8.7c0 0 .8-1.5 2.3-1.2 0 0 3.3 1 .8 5.5 0 0-1.4 2.9-.8 4.5zm10.5 5.3s-7.4-3.4-5.1-9.2c0 0 .7-2 2.4-1.8 0 0 2.8.2 2.3 4 0 0-.9 3.6.4 7z"/>
|
||||||
|
<path d="M428.7 303.6s-2.3 1.7-3.1 2.8c0 0-1-1.2-2.2-1.8 0 0 1.5-.3 2.2.3 0 0 1.2-1 3-1.3z"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M435.2 323.6s6-1.4 4.8-7.5c0 0-.6-2.6-3-2.2 0 0-3 .8-1 4.6 0 0 1.1 2.6-.8 5.1z"/>
|
||||||
|
<path fill="#f5ce00" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="m461.6 196.7 5.7 1.9s5.4 2.3 12.3 0l5.2-2-3.5 5.4v2.9l2.1 3.2s-1.3.6-5.4-2c0 0-4.4-3.4-9.6 0 0 0-3 2-5.4 2l3.5-3.8-1.3-3.5-3.6-4.3z"/>
|
||||||
|
<path fill="#fff" d="M437.5 316.5s.2-.4 0-.5l-.5.2s-.6 1 0 2.4c0 0 .5.9.3 1.8v.4s.3-.1.3-.4c0 0 .4-.8-.2-1.8 0 0-.6-1.4.1-2.1"/>
|
||||||
|
<path fill="#d40000" stroke="#000" stroke-linejoin="round" stroke-width="1.2" d="M518 207.7s-4.2 2.2-6-.3c0 0-2.1-4.4 2.9-6 0 0-2.8-3.2.1-6.2 0 0 1.6-1.6 4.2-.5 0 0 1.2-2.3 3.7-2.2 0 0 2.4 0 3 2.5 0 0 3.3-1.2 5 .6 0 0 2.8 3.2-1.2 5.8l-2.9.3-2.9-1.4-3 .9s.8 3.2-3 6.5z"/>
|
||||||
|
<path fill="#f5ce00" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M484.8 204.4s0 .8-.8 1c0 0-1 .3-1.5-.9v-.3c0-.5-.3-2 1.4-3.3 0 0 2.6-2.1 7.6.5a192 192 0 0 0 14.8 7.5s3.1 1.6 7.9 1.8c0 0 6.5.5 9.3-4.1 0 0 2.1-3.5 0-5.8 0 0-.9-1-2.3-.8a3 3 0 0 0-1.8 1.2s-1 1.4.1 2.5c0 0 1.6 1 2.2-1 0 .2.5 1.7-.4 2.9 0 0-4 6-14.9-.1l-14.3-8.1s-7.1-3.7-11.6 2.1c0 0-3.4 4.8 1.1 7.9 0 0 3.4 2 5.4-1.2 0 0 1.8-3.1-1-4.4 0 0-2.4-1.2-3.3 1.2-1 2.4 1.8 3 2 1.3 0 0 0-.4.3 0z"/>
|
||||||
|
<path fill="#e4cb5e" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M428 229.2v-9c0-.8 0-1.2 1.2-2.2 1-.8 2.1-2.3 3.7 1.7 0 0 3.2-3.5 4.2-4 0 0 2-1.5 3.3.7 0 0 1.6-2.6 3-3.2 0 0 3.2-2 3.3 4.2l2.5-2.4s2-1.5 4.2.7c0 0 3.5 3.6 4 4.6 0 0 .8 1 1 2.7 0 0 0 2 1 3 0 0 1.1 1 2.2 1.2 0 0 2.5.1 3.6 2.6 0 0 .3-.5 1.6 11.2v21.5l-14.4 17.1-23.1-6.6-9.2-3.9-2.2-6.6 9-5.9 4.8-13-1.5-9.3-2.2-5.2z"/>
|
||||||
|
<path fill="#784421" stroke="#000" stroke-width="1.2" d="m491.6 240.4 1.2-1.5 2.5-2s4 10.4 4.2 12.7v3.2s6 1.6 7.2 10.7l-5 9.2-7.8-4.4-2.3-1.5z"/>
|
||||||
|
<path fill="#fff" d="M418.3 312.4s.2-.3 0-.6l-.5.4s-1.5 1.7-.2 4.4c0 0 0 .4.3.2 0 0 .3 0 0-.3 0 0-1.1-2.4.4-4z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="M441.7 310.6s.1-.8-.4-1.6q-.2 0-.2-1.5m5.6-90.1s0 2.3.7 4c.6 1.6 2.9 4.5 3 5.9"/>
|
||||||
|
<path fill="#d40000" stroke="#000" stroke-linejoin="round" stroke-width="1.2" d="M501 295.5s3.6.1 5.5 1.2c2 .8 3.4 1.8 5.5 3.2 0 0 1 1.3 5.2.5 0 0 7.2-1.7 7.2 4 0 0 8.7.6 5.2 8.3 0 0-2.4 6-7.4 1.8 0 0 3.2 4.5-3 6.3 0 0-4.4 1.2-5.7-3.5 0 0-.8-1.8 1-4 0 0-2.1-.4-3.5-2.7 0 0 4.5 5.8-.5 8 0 0-6.3 1.5-6.7-5 0 0 .5-3.2-2-4.3 0 0-5-1.2-7.2-6.6 0 0 7.2-3.9 6.5-7.2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="m443.8 227.4 6 47.8m-4.5-47.9 6.6 44.6m-5.5-44.8 7.8 44.2"/>
|
||||||
|
<path fill="#784421" stroke="#000" stroke-width="1.2" d="m458.6 267.5 2.2-.7-14-48h-.5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="m450.7 225.3 14.1 35.5m-15-35 13.6 37"/>
|
||||||
|
<path fill="#fff" d="M427.6 321.6s-1.7-1.5-1.4-4.3c0 0 0-.6-.3-.7 0 0-.3-.1-.3.6 0 0-.5 2.9 1.5 4.5q0 .2.4.2v-.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="m451.5 224.8 14.3 32.4m-18.3-30.4 9 43.7"/>
|
||||||
|
<path fill="#f5ce00" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M520 259c-6.3-3.6-7.7-11.4-7.7-11.4a25 25 0 0 1 1.6-14.3c4-8.6 11.1-10.3 11.1-10.3s-6.9 4.8-8.8 11.2c0 0-1.4 5.5-.6 10.7.8 5.5.5 4.1 1.8 8.3l2.6 6z"/>
|
||||||
|
<path fill="#784421" stroke="#000" stroke-width="1.2" d="m492.7 238.7-26.6 8.5-1.4 12c-6.3 9.1-19.2 10.7-19.2 10.7l10 10.7 19 4 10.2-7.8 9.2-8.4c-1-4.5-.5-11.2-.5-11.2 0-1.2.5-3.8.5-3.8s-1.5-10.7-1.2-14.5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="M465 256.2s17.6-5.5 28-10m-36.4 20s22-4 37.3-12.8l5.6-3.8"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="m499.5 252.8-6 4.4s-24.5 11.2-43.2 11.6m43.1-3.6S474.6 275 464 275"/>
|
||||||
|
<path fill="#d40000" stroke="#000" stroke-linejoin="round" stroke-width="1.2" d="M515.8 238s5.6-2.5 8.4 0c0 0 3.6-2.2 7.6 0 0 0 3.6 2.4 6.2 5.2 0 0 1.8 1.5 1.2 6.4 0 0-1 3.5-.3 5.1 0 0 1.1-.2 3.5 2.8 0 0 3.3 3.5 6 .3 0 0-.8 4.6-6 3.8 0 0 2.4 2 .3 7 0 0-1.6 3.6 1 8.2 0 0-4.6-1.8-4.4-7.2 0 0 .4-3.6-1-6 0 0 1.4 2.2-1.7 7.1 0 0-2.4 3.6-.4 7.2 0 0-4.5-1.6-4-7 .3-5.4 3-3.2 1.3-8.2 0 0-.4 2.6-1.7 4a7 7 0 0 0-2.4 7s-2.6-3.4-2-6.8c.6-3.5 1.2-4.4 1.2-4.4l-8.3-4.8-4.2-7.5z"/>
|
||||||
|
<path fill="#fff" stroke="#00247d" stroke-miterlimit="10" stroke-width="1.2" d="M420.6 268.4s.8-3.7 4.3-2.4c0 0 1.2-6 7.7-6.2s6.8 6.4 6.8 6.8c0 0 2-2.8 5.1-2.5 0 0 5.5-.3 3.6 8.5l1 1.2s4.1-9.9 12.7-7.4c0 0 8.4 2.6 3 10.7 0 0 4.2 5.5 7.6 5 3.6-.5 6.7-1.6 10.2-7.8 3.6-6.2 11.7-7.2 13.7-6.8 2 .3 3.7 1.8 4 3.2 0 0 4.3-14.3 19.8-12.2l6.2 3.3 2.4 1-3.6 10-8.4 19.2-6.6 2-6.8-3.9-2.4 1.2v5.9l-9 6.4-6.2 2.5-6.8 4.8-1.7 4.2s-3.7-1.7-8 0l-1.4-3.6-4-3.7-15.6-7.4-2.7-9-2.9-1.3-3.2 3.6-4.5.6-7-4.8-7.4-21z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M488.2 198s-6-.2-5.7 6"/>
|
||||||
|
<path fill="#f5ce00" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M527 263.3c-10.2-3.2-13-14.3-13-14.3a26 26 0 0 1 1.2-15.5c4.8-11 12.7-11 12.7-11a4 4 0 0 1 4.5 3.2c.4 2.4-1.3 3.3-1.3 3.3-2.7 1.5-4.5-.5-4.5-.5-1.2-1.6-.3-2.9-.3-2.9.7-1 2-.4 2-.4 1.3.2 1 1.6 1 1.6s.4-1.6-1.2-1.8c0 0-3.2-.7-6.8 3.8 0 0-4.7 6.1-4.7 14.1 0 0-.6 14.8 14.7 18.6 0 0-1.6 2.4-4.6 11.1 0 0-3.3 11.2-6.4 17.3 0 0-4.3 8.6-13.9 5.1 0 0-5.6-2.5-5.6-6.5 0 0-.3-3.8 3-4 0 0 3.3-.3 3.3 2.5 0 0 0 3-3.5 2.4 0 0-1.2-.3-1.1-1.5 0 0 .2-1.2 1.8-.6 1.5.6 0 0 0-.1 0 0-.6-.3-1.2 0 0 0-.6 0-.6 1q-.2.2.7 1.1l1.7.2s1 2 3 2.7a8 8 0 0 0 7.1-1.2 11 11 0 0 0 3-4.3 113 113 0 0 0 5.2-13.5s1.8-5.7 3.4-8.7z"/>
|
||||||
|
<path fill="#f5ce00" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M498 291.4s2-.2 2 1.5c0 0-.1 2.4-3.2 1.9 0 0-3-.6-2-4 0 0 .8-2.3 3.6-2 0 0 2.8 0 4 3.8 0 0 1 3.3-.7 6-1.8 3-6.2 5-8.4 6 0 0-8.7 3.3-11.3 5.3 0 0-4 2.9-2.2 5.6 0 0 .7 1.1 1.7 1.1 0 0 1.2 0 1.3-1.2 0 0 0 .7-.7 1 0 0-.8.4-1.7-.3 0 0-1-1-.2-2.4 0 0 1-1.6 3.6-.6 0 0 2 1.1 1.2 3.3 0 0-.9 2.1-3.4 2 0 0-1.9 0-3.2-1.2-2-2.1-2-6-.2-8 0 0 1.5-2.1 4.7-3.6l9-3.5q4-1.5 6.4-4.1s1.3-1.4 1.8-4c0 0 .4-2-.8-2.4l-1.2-.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M498.2 291.4s2.1-.4 3.5 2.2c0 0 .7 1.5.8 2.5m.7-9s-2.1.3-1 3.4c1.1 2.9 3 3.7 3.5 4.2"/>
|
||||||
|
<path fill="#f5ce00" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M461.6 204.4s0 .8 1 1c0 0 .8.2 1.3-.9v-.3c0-.5.3-2-1.2-3.3 0 0-2.8-2.1-7.8.5 0 0-4 2-6.7 3.6 0 0-7.1 3.8-8 4 0 0-3.2 1.6-8 1.8 0 0-6.5.5-9.2-4.1 0 0-2.2-3.5 0-5.8 0 0 .8-1 2.2-.8q.9 0 1.8 1.2s1 1.4-.1 2.5c0 0-1.6 1-2.2-1 0 .2-.3 1.6.5 3 0 0 4 5.9 14.8-.3l14.3-8s7.2-3.7 11.6 2c0 0 3.4 4.8-1 8 0 0-3.3 2-5.5-1.2 0 0-1.6-3 1-4.4 0 0 2.4-1.2 3.5 1.2 1 2.4-2 3-2.2 1.3 0 0 0-.4-.1 0zm-35.2 54.7c6.3-3.5 7.8-11.5 7.8-11.5 1.4-8-1.7-14.3-1.7-14.3-4-8.6-11-10.3-11-10.3s6.8 4.8 8.7 11.2c0 0 1.6 5.5.6 10.7-.8 5.5-.5 4.1-1.8 8.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M458.4 198s6-.2 5.6 6"/>
|
||||||
|
<path fill="#f5ce00" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M419.3 263.3c10.3-3.2 13-14.3 13-14.3a26 26 0 0 0-1.1-15.5c-4.8-11-12.7-11-12.7-11a4 4 0 0 0-4.5 3.2c-.3 2.4 1.3 3.3 1.3 3.3 2.8 1.5 4.6-.5 4.6-.5 1.1-1.6.2-2.9.2-2.9-.7-1-2-.4-2-.4-1.2.2-1 1.6-1 1.6s-.3-1.6 1.2-1.8c0 0 3.2-.7 6.8 3.8 0 0 4.8 6.1 4.8 14.1 0 0 .6 14.8-14.8 18.6 0 0 1.7 2.4 4.6 11.1 0 0 3.4 11.2 6.6 17.3 0 0 4.2 8.6 13.7 5.1 0 0 5.6-2.5 5.6-6.5 0 0 .4-3.8-3-4 0 0-3.3-.3-3.3 2.5 0 0 0 3 3.6 2.4 0 0 1.2-.3 1-1.5 0 0-.2-1.2-1.7-.6-1.6.6 0 0 0-.1 0 0 .5-.3 1.1 0 0 0 .6 0 .6 1q.2.2-.7 1.1l-1.5.2s-1 2-3.2 2.7a8 8 0 0 1-7-1.2 11 11 0 0 1-3-4.3 114 114 0 0 1-5.3-13.5s-1.8-5.7-3.2-8.7l-.6-1.2z"/>
|
||||||
|
<path fill="#f5ce00" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M448.5 291.4s-2-.2-2 1.5c0 0 0 2.4 3.2 1.9 0 0 3-.6 1.9-4 0 0-.7-2.3-3.6-2 0 0-2.7 0-4 3.8 0 0-.9 3.3.8 6 1.8 3 6.2 5 8.3 6 0 0 8.7 3.3 11.4 5.3 0 0 3.9 2.9 2.1 5.6 0 0-.6 1.1-1.7 1.1 0 0-1.2 0-1.3-1.2 0 0 0 .7.7 1 0 0 .9.4 1.7-.3 0 0 1-1 .2-2.4 0 0-1-1.6-3.4-.6 0 0-2.2 1.1-1.2 3.3 0 0 .7 2.1 3.2 2 0 0 2 0 3.2-1.2 2-2.1 2-6 .4-8 0 0-1.7-2.1-4.8-3.6-1.9-.6-5.5-2.1-9-3.5q-4.1-1.5-6.5-4.1s-1.3-1.4-1.8-4c0 0-.3-2 .9-2.4l1.2-.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M448.2 291.4s-2.1-.4-3.4 2.2c0 0-.7 1.5-.9 2.5m-.7-9s2.2.3 1.1 3.4c-1.2 2.9-3 3.7-3.6 4.2m27.4-91.1s5.2-3.3 10.4.3m-39.7 31.3s.7-3-2.6-8c0 0-2.2-4.7-3.2-7.5m7.4-3.3 2.3 6.6 3 6.8"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="M441.2 238.3s2 2.1 1.8 5.6m-8.1-7.9s2.1 1 1.9 6.8c0 0-.1 4 3 6.2m-5.3 1.4s6.7-.7 7.2 3.5c0 0 0 4 2.4 4.3 0 0 3.3.3 4 3.6m-11.4-6.8s1.2 1.7 2.4 2.6m6.2-7s1.6 2.7 2 4.1m5.3-35s1.6 2.1 3.1 3m3.1 9.6s3.4 1.6 3 10c0 0-.6 5 .9 7.7"/>
|
||||||
|
<path fill="#784421" stroke="#000" stroke-width="1.2" d="m443.1 226.6.4.8s6 .3 9.1-3.8l-.3-.8s-5.3-.3-9 3.8z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="M464.7 258.5s20.6-6 28.6-9.6"/>
|
||||||
|
<path fill="#784421" stroke="#000" stroke-width="1.2" d="m462.5 220.5-.2-.6 10.1-4 .3.8z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="m463.4 220.1 6.8 33.3m-5.6-33.6 7.7 33m-6.4-33.6 8.4 32.9m-5-34.1 11.5 24.6m-10-25 12.3 24.2M471.8 217l13.4 24.3m-15.5 12.4 5.2-1.6m-9.1-1.6s15.2-4.5 27-9.2M465.4 252s19.5-5 27.2-8.8m-21.2 24.6 1 3.3 2.8-1-1-3.1m5-1.3 1 3 2.8-.9-1-3m-16-17.6-.2-4.2 25.5-7.7 1.3 3.6m-16 .8 1.3 3.8m7.6-6.4 1.6 3.6m.5-4.3 1.6 3.3m.4-4 1.6 3.6m0-4 2.8-1.6 1.2 3.1m-2.6-2.3 1.4 3.3"/>
|
||||||
|
<path fill="none" stroke="#00247d" stroke-miterlimit="10" stroke-width="1.2" d="M429.5 283.1s-5-5.1 0-10c0 0-5.5-2.6-4.6-7m13.6 21s-5.3.7-3.7-9.4c0 0-2.2 4-2.9 6a5 5 0 0 0 2 5.4c.9.6 5.5 1.8 7-1.2m-8.3-16.2s-2.1 1.9-.6 5.4m2.8-5s.2 3.5 2 5.4m-.5-6.7s0 4 2.9 6.6m-1.5-8s0 4.5 3.4 7.4m13.5-.7s1.6-3.9 5.2-3.6c0 0-1.7.8-1.7 2.5 0 0-.2 3 2.9 3.2 0 0 2.5.4 3.8-1"/>
|
||||||
|
<path fill="none" stroke="#00247d" stroke-miterlimit="10" stroke-width="1.2" d="M463.1 293.1s-8.9-4.2-8-10.1c0 0 .4-3.6 4-5.4m-2 3.8s-.8 2.5 1 4.3m1.1-6s-1.2 2.8.7 4.8m1.5-6.3s-1.4 2.4.2 4.5m-8.4 11s4.8 4.3 8.6 4.3m-10.1-2.9s4.5 3.6 7.4 4.2m-9-3.2s5.5 4.9 9 5.2m13.7 4.6s-5.5 3-1.2 6.2m-2.4-9.4 9-4.6m-6.7 6.2s9-5.5 11.7-6.7m-7 7.2s7.7-5.2 11.9-7.5m-6.8 7.1s5.3-4 10.5-7.1m1.7-5.1s-2.6-3.9-11.2 0c0 0 3.3-2.9 9.5-5.2m8.5-16.3s.8 3.1 0 5"/>
|
||||||
|
<path fill="none" stroke="#00247d" stroke-miterlimit="10" stroke-width="1.2" d="M486.5 285.7s1.8-2.6 6.3-4c0 0 1.2 3 4.8 2.5 0 0 5.2-1.1 3.6-7 0 0-1.1-4.2-7.2-4.7m18.1-7.3s6.1 1.2 6 6.1c0 0 .6 7.6-7.4 9m6.6-12s1.6-3.7 5.5-5.2m-4.6-2.2s1.7 2.8 2.8 3.1m-3.6-1s1.5 2.1 2 2.5m-2.6-.5 1.5 1.6M485 235.8s1.2-2 4.3-1.2m12.1 15.8s2-7.8 8.5-5m-2.6-.6s.6-3.6-1-5.5m-3.6 5.2s.9 1.5.1 2.7"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M516.2 325s7.4-3.3 5.1-9.1c0 0-.7-2-2.4-1.8 0 0-2.8.1-2.3 4 0 0 1 3.8-.5 7z"/>
|
||||||
|
<path fill="#fff" d="M518.8 321.6s1.7-1.5 1.5-4.3c0 0-.2-.6 0-.7 0 0 .4-.1.4.6 0 0 .5 2.9-1.4 4.5q0 .2-.4.2-.2 0 0-.3z"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M526.7 319.8s5.2-3.2 2.4-8.7c0 0-.9-1.5-2.4-1.2 0 0-3.2 1-.7 5.5 0 0 1.2 2.9.7 4.5z"/>
|
||||||
|
<path fill="#fff" d="M528 312.4s-.2-.3 0-.6l.4.4s1.5 1.7.3 4.4c0 0-.1.4-.5.2v-.3s1.2-2.4-.2-4z"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M511 323.6s-5.8-1.4-4.7-7.5c0 0 .6-2.6 3.1-2.2 0 0 3 .8 1 4.6 0 0-1.1 2.6.7 5.1z"/>
|
||||||
|
<path fill="#fff" d="M508.8 316.5v-.5s.4 0 .5.2c0 0 .6 1 .1 2.4 0 0-.5.9-.2 1.8l-.3.4-.2-.4s-.3-.8.2-1.8c0 0 .6-1.4 0-2.1z"/>
|
||||||
|
<path d="M515.2 312.3s3.4-1.9 6 .3c0 0-.2-1.2-2.4-1.7 0 0-1-3.2-4.2-2.9 0 0 1.2 1.6 3 2.8 0 0-1.8.2-2.4 1.5m2.4-8.7s2.4 1.7 3.2 2.8c0 0 .9-1.2 2-1.8 0 0-1.2-.3-2 .3 0 0-1.1-1-3-1.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="M504.8 310.6s-.3-.8.3-1.6c0 0 .4-.6.2-1.5m23.4-48s3.5-.2 4-3m6-1.8s-1.2 0-2.2 1.2c0 0-.9 1.2-2 1"/>
|
||||||
|
<path d="M523.6 249.2s1.8 0 2.7-.8c0 0 1.1-.7 1.7.4 0 0 1 1.6-.8 2.3 0 0-2.2 1.2 1.3 3.4 0 0-4.2-1.6-2.4-3.6 0 0 1.8-1.2 1-1.7 0 0-.2-.4-.9 0-.7.6-2.1.4-2.6 0"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M526 205.6s.8-1.2.5-4.4 2.6-3.7 3.6-2.8c0 0 1.2 1 0 3.1a8 8 0 0 1-4.1 4.1z"/>
|
||||||
|
<path fill="#fff" d="M527 203s1.2-1.1.6-3.6l.2-.2h.3s.6 2.8-.6 4.2c0 0-.2.3-.5 0z"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M525.1 204.4s0-1.4-2.3-3.6c-2.4-2.4-.5-4.4 1-4.4 0 0 1.3 0 2 2.3a8 8 0 0 1-.7 5.7z"/>
|
||||||
|
<path fill="#fff" d="M524.8 201.4s0-1.6-2.2-3.3v-.4h.4s2.3 1.8 2.3 3.8q0 .2-.3.3-.1.1-.2-.4"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M522 203.3s-.7-1-3.5-2.1c-2.9-1-2.2-3.5-1-4 0 0 1.3-.5 2.6 1.1a8 8 0 0 1 1.8 5z"/>
|
||||||
|
<path fill="#fff" d="M520.6 201.3s-.6-1.4-3.2-2.1l-.2-.3.3-.1s2.9.6 3.7 2.4l-.1.3s-.3 0-.4-.2z"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M523.7 204.4s-1.3.5-4.4-1-4.4 1.1-3.8 2.3c0 0 .5 1.6 2.8 1.2a8 8 0 0 0 5.4-2.4z"/>
|
||||||
|
<path fill="#fff" d="M516.6 205.4s1.4 1 3.9 0h.3s0 .2-.2.3c0 0-2.6 1.2-4.4.1v-.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M519.3 194.7s1.4.8 1.2 2.5"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="M526 195s.6 1.9 0 3"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M420.2 205.6s-.8-1.2-.5-4.4c.4-3.4-2.5-3.7-3.5-2.8 0 0-1.2 1-.2 3.1a8 8 0 0 0 4.2 4.1z"/>
|
||||||
|
<path fill="#fff" d="M419.1 203s-1-1.1-.6-3.6v-.2h-.4s-.5 2.8.7 4.2c0 0 .2.3.3 0z"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M421.2 204.4s0-1.4 2.4-3.6c2.3-2.4.4-4.4-1-4.4 0 0-1.6 0-2 2.3a8 8 0 0 0 .6 5.7z"/>
|
||||||
|
<path fill="#fff" d="M421.5 201.4s0-1.6 2-3.3c0 0 .3-.3 0-.4h-.2s-2.4 1.8-2.4 3.8q-.1.2.4.3.1.1.2-.4"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M424.3 203.3s.6-1 3.5-2.1c2.8-1 2-3.5.9-4 0 0-1.2-.5-2.6 1.1a8 8 0 0 0-1.8 5z"/>
|
||||||
|
<path fill="#fff" d="M425.6 201.3s.6-1.4 3.2-2.1l.2-.3-.2-.1s-2.9.6-3.7 2.4v.3s.4 0 .5-.2"/>
|
||||||
|
<path fill="#64b4d1" stroke="#00247d" stroke-miterlimit="2.6" stroke-width="1.2" d="M422.5 204.4s1.4.5 4.4-1 4.4 1.1 4 2.3c0 0-.7 1.6-3 1.2a8 8 0 0 1-5.4-2.4z"/>
|
||||||
|
<path fill="#fff" d="M429.6 205.4s-1.3 1-3.9 0h-.2v.3s2.8 1.2 4.5.1q.2 0 0-.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="2.6" stroke-width="1.2" d="M427 194.7s-1.5.8-1.2 2.5"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.2" d="M420.2 195s-.6 1.9 0 3"/>
|
||||||
|
<path d="M486.7 177.6s1 2.7.6 5.2c-.3 2.4.2 2.9 1 3.4.7.3 2.4-.4 2.5-2.2 0 0 2 3.8-1.8 6.3 0 0-2.7 1.7-5-.4-.8-.9-1.2-3-.7-5 0 0 .6-2.7-.5-5.5 0 0 1.6 1.6 1.2 4.9 0 0-.8 6 3.6 5.6 0 0 3 0 3-3.6 0 0-1.1 1.2-2.4.8a2.4 2.4 0 0 1-1.8-2.8c.1-1.6.6-4.3.3-6.7M482 187s-.4 4.8-4.6 5.9c0 0 .6-.9-.3-3.3 0 0-1-1.2-1-3.3 0 0-1.2 1 .3 3.8 1 1.7 0 3.6-.1 3.7-.4.1 6.9-1 5.6-6.7zm-5.6-4.5s-1.1-1.2-1.2-3.8c0-2.7-.5-3.4-.9-3.6 0 0 .5 2.3.4 3.6a7 7 0 0 0 .4 3s-2.4.3-3.9-3c-1.3-3.3-2.7-3-3.4-3 0 0 .8.1 2.4 3 1.6 3 2.1 3.7 6.2 3.7zm-10.8-7s1.2 3.2 1.4 4.8c0 0-3.1-1-4-3.5 0 0-3.2 1-2 4.7 0 0-2.9-1-4.2-3.2 0 0 1.6 1.2 3.1 1.7 0 0-.2-3.1 3.6-4.2 0 0 .6 2.7 2.6 3.3zm5-5.4s1.2 1.5 3.4 1.5a4 4 0 0 0 3.2-1.5s-.1 2.6-3.2 2.7c0 0-3.3 0-3.3-2.7zm-13.4 18.5 2 1.9s1.2 1.2 2-.3c0 0 1.3-2.4 2.9-2.2 0 0-1.2.7-2.4 3 0 0-.2.7-1.2.8-.5 0-1 .2-1.8-.7q-.2 0-1.5-2.4zm16.7-29.4c-3.2 0-.5 2.4-.5 2.4 0 4-3.1 5.4-5.3 4.2s-.4-4-.4-4-2.4 1.5-1 3.9c1.6 2.4 5.7 1.2 7.2-.9 1.5 2.2 5.7 3.3 7.1.9 1.7-2.4-.8-4-.8-4s1.8 2.9-.5 4c-2.1 1.3-5.4 0-5.2-4 0 0 2.7-2.5-.5-2.5zm2-4.4s1.2 2 1 5c0 0 .9-3-1-4.9zm-10 4.7s-.3-2.2-2.4-2.4c0 0 1.9 1.3 2.4 2.4m16 0s.3-2.2 2.5-2.4c0 0-1.8 1.3-2.4 2.4zm-19.2 3.2s1.6.5 2.6-.7zm-5.4-13.1s1.9-.1 3.8 2.7c0 0-1.9 1-2.4 1.6 0 0 0-1 1-1.7 0 0-.4-1.4-2.4-2.5zm32 0s-2-.1-3.9 2.7c0 0 2 1 2.4 1.6 0 0 0-1-1-1.7 0 0 .4-1.4 2.5-2.5zm-14.7 2.2s1.7.5 3.8-.7c0 0 2.6-1.4 4.3 0 0 0-1.4-.7-4.3.7 0 0-2.7 1.6-3.8 0"/>
|
||||||
|
<path fill="#fff" d="M476.4 153.6s1.4-2.5 6-2c0 0-1.3 3.6-6 2"/>
|
||||||
|
<ellipse cx="479.4" cy="152.8" fill="#784421" rx=".8" ry="1.1"/>
|
||||||
|
<ellipse cx="479.4" cy="152.8" rx=".5" ry=".7"/>
|
||||||
|
<path d="M466.8 148.1s2.8.3 4.6 1.2c0 0 1.8 1.2 3.8-.3 0 0 2.2-1.4 3.7-3.1 0 0-3.5 2.3-4.8 2.7 0 0-1.2-1-1.5-2.3 0 0 0-1 2-2.7 0 0-2.5.8-3 2.9a7 7 0 0 0 1.4 2.3s-.4.3-1.6-.6c0 0-2.8-.8-4.6 0zm7.2 17c-2.4 2.1 0 1.9 0 1.9s2.4.2 0-2zm-1.4-13.2s-1.8.5-4-.7c0 0-2.6-1.4-4.3 0 0 0 1.5-.7 4.3.7 0 0 2.9 1.6 4 0"/>
|
||||||
|
<path fill="#fff" d="M470.8 153.6s-1.5-2.5-6-2c0 0 1.2 3.6 6 2"/>
|
||||||
|
<ellipse cx="467.7" cy="152.8" fill="#784421" rx=".8" ry="1.1"/>
|
||||||
|
<ellipse cx="467.7" cy="152.8" rx=".5" ry=".7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width="1.2" d="M560 130.6v161.6c0 43.1-86.2 57.2-86.2 57.2s-86.4-14-86.4-57.2V130.6z"/>
|
||||||
|
<path fill="#006" d="M0 0h320v240H0z"/>
|
||||||
|
<path fill="#fff" d="m37.5 0 122 90.5L281 0h39v31l-120 89.5 120 89V240h-40l-120-89.5L40.5 240H0v-30l119.5-89L0 32V0z"/>
|
||||||
|
<path fill="#c8102e" d="M212 140.5 320 220v20l-135.5-99.5zm-92 10 3 17.5-96 72H0zM320 0v1.5l-124.5 94 1-22L295 0zM0 0l119.5 88h-30L0 21z"/>
|
||||||
|
<path fill="#fff" d="M120.5 0v240h80V0zM0 80v80h320V80z"/>
|
||||||
|
<path fill="#c8102e" d="M0 96.5v48h320v-48zM136.5 0v240h48V0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,36 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-bn" viewBox="0 0 640 480">
|
||||||
|
<path fill="#f7e017" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fff" d="M0 33.3v213.4l640 100V233.3z"/>
|
||||||
|
<path fill="#000001" d="M0 146.7v100l640 200v-100z"/>
|
||||||
|
<g fill="#cf1126" transform="translate(-160)scale(.66667)">
|
||||||
|
<path d="M695.7 569.7a117 117 0 0 1-49.4-17.2c-2.4-1.6-4.6-3-5-3s-.6 1.9-.6 4.1c0 6.4-2.6 9.6-9 11.3-6.2 1.6-15.6-1.6-23.2-8a68 68 0 0 0-24.7-13.5 40 40 0 0 0-28 3.6 9 9 0 0 1-2.8 1.3c-1.1 0-1-6.9.2-9 1.5-3 5.1-5.8 9.4-7.3 2.2-.8 4-1.8 4-2.3s-.8-2-1.7-3.6q-4.3-7.9 3.4-13.9c5.2-4 14-4.6 21.7-1.7l4 1.4c1 0 .4-1.5-2.4-5.6-3.2-4.7-3.9-7-3.5-12.7a15 15 0 0 1 13.5-13.5c5.8-.4 9.4 1.6 18 9.7a144 144 0 0 0 86 41.6c8.3 1 24.8.5 34.5-1a156 156 0 0 0 81.8-40.8c6.4-6 9.4-7.6 14.7-7.6 4.5 0 7.7 1.4 11 5 3 3.3 4 6.4 3.6 11.5-.2 3.2-.7 4.7-2.6 7.9-2.8 4.5-2.3 5 3.2 2.8 7.6-3 16.9-1.6 21.9 3.2 4.4 4.2 4.8 8.4 1.4 14-1.3 2.1-2.3 4-2.3 4.4 0 .6 1 .8 5.5 1.6 6 1 9.5 5.4 9.5 12.2 0 2-.3 3.7-.6 3.7s-2.6-.9-5-1.9c-7-2.9-11-3.6-19.2-3.5-6.2 0-8.3.3-12.6 1.7a58 58 0 0 0-19.5 11.5c-6.4 5.7-10.4 7.5-16.6 7.4q-8.7 0-11.8-5c-1.1-1.8-1.3-2.8-1-6.8.2-2.6.1-4.7 0-4.7-.3 0-2.5 1.4-5 3.1A81 81 0 0 1 778 560a182 182 0 0 1-82.3 9.7"/>
|
||||||
|
<path d="M706.3 525.2a136 136 0 0 1-97.9-55.7c-24.4-33.2-32-77.1-24.6-117.2 4-18.3 12-36.6 25.5-49.6a115 115 0 0 0-8.7 74.3c9 49.8 51 91.9 101.3 99.2 20 5.7 40.5-.4 59.5-6.5 42-14.8 74-54.6 77.8-99.1 3.3-24-.3-49.1-11.2-71 6.2 3.3 14 16.2 18.6 24.8 16 31 16.7 68.1 7.3 101.2-12.8 42.1-45 79-87.5 92.4a166 166 0 0 1-60 7.2z"/>
|
||||||
|
<g id="bn-a">
|
||||||
|
<path d="M512 469.9c-2.5-.4-5.3 2.1-4.3 4.7 1.8 2.6 5 4 7.8 5.2a54 54 0 0 0 23.2 3.6 50 50 0 0 0 17-3c3-1 6.8-2 8-5.4 1.3-2.1-1-4.3-3.1-3.9-3 .7-6 2-9 2.9a58 58 0 0 1-20.3 2 54 54 0 0 1-14.4-4.2c-1.6-.7-3.1-1.7-4.9-1.9"/>
|
||||||
|
<path d="M514.8 459.5c-2.5-.4-4.7 2.6-3.7 5 2 2.8 5.3 4.3 8.4 5.6a42 42 0 0 0 17 2.9h1.5a38 38 0 0 0 14.4-2.8c2.7-1.1 6.1-2.2 7.3-5.2.9-1.7.2-4.1-2-4.3-1.8 0-3.5 1.2-5.3 1.7a44 44 0 0 1-20.6 3.2c-4.4-.5-8.5-2.1-12.5-4-1.5-.7-2.8-1.8-4.5-2z"/>
|
||||||
|
<path d="M518.3 449.6c-2.2-.3-3.7 2.2-3.3 4.1.3 1.8 1.8 3 3.1 4a30 30 0 0 0 18.6 5.3h1.6a28 28 0 0 0 12-2.8c2.5-1 5.4-2.3 6.3-5.2.4-1.3.6-3.2-.9-4-1.6-.8-3.1.5-4.5 1a34 34 0 0 1-15.5 3.9 27 27 0 0 1-13.1-4c-1.5-.7-2.7-2-4.3-2.3"/>
|
||||||
|
<path d="M481.5 302.7c-3.2 3.3-.7 9.3-1 13.5 1.8 13.2 3.9 26.5 8.8 39 6 12 18.8 18.5 26.5 29.2 2.8 5.1 1.8 11.3 2.4 17q.5 23 0 46c7 3.6 14.5 7 22.5 5.7 4.7-1.1 13.5-1.8 14.5-6.5l-1-79.5c-2.7-8.1-11-12.3-17.1-17.5a156 156 0 0 1-14.2-16.1c-2.6-4.5-12.9-6-9.2 1.6 2.2 6.7 7.7 11.6 9.1 18.6.3 3.9 5 11 1 13.2a25 25 0 0 0-10.7-10c-4.4-3.3-11.7-4.7-13.3-10.5a43 43 0 0 0-11-22c1.5-7.4 0-16.7-6.4-21.5z"/>
|
||||||
|
<path d="M491.4 304.2c-3 .5-2.8 4.2-1.5 6.2a27 27 0 0 1 1.1 13.4 44 44 0 0 1 10.6 21.7c0 3 3.2 4 5.3 5.5 4.9 3.1 10.3 5.4 14.7 9.3.9 1 1.6 2 1 0-.7-2.6-1-5.4-3-7.3-2.8-3-6.2-5.6-10.2-6.4-.3-4.2-2.3-8-4.1-11.6-2-3.5-4.1-7.2-7.5-9.4 0-6.1 0-12.5-2.6-18.2-.8-1.4-2-3.1-3.8-3.2"/>
|
||||||
|
<path d="M499.7 306.6c-2 .6-1.6 3.2-1 4.7a54 54 0 0 1 1 13.2c3.9 3 6.2 7.4 8.4 11.6q2.2 4.2 3.1 8.9c3.1 1 5.8 3 8.2 5-1-2.8-3-5-4.5-7.7s-3-5.6-3.7-8.7c-3-3.1-4.6-7.6-4-12 .2-4.7-1.3-9.6-4.5-13.2-.8-.8-1.8-1.7-3-1.8"/>
|
||||||
|
<path d="M509.2 308c-1.2.2-1.8 1.2-2.4 2.1-.3.9.8 1.8 1 2.8a22 22 0 0 1 1.4 10.4q0 3.9 2 7a4 4 0 0 1 3.5-2.8c.5 0 1.4.2 1-.7q-.4-7.3-2.8-14a10 10 0 0 0-2.8-4.5q-.4-.4-1-.3z"/>
|
||||||
|
</g>
|
||||||
|
<use xlink:href="#bn-a" width="100%" height="100%" transform="matrix(-1 0 0 1 1440 0)"/>
|
||||||
|
<path d="M715.7 476a36 36 0 0 1-29.9-24c.3-2.2 3 1.2 4.3 1.5a19 19 0 0 0 8 2.6c3.5 1.5 5.7 5 9.1 6.9 1.6 1.2 7.2 3.6 6.1-.3-1.3-2-2.2-4.6-1-7 1.8-4.1 4.7-7.7 7.7-11.2 2.1-.7 3.6 3.6 5.1 5 2.1 3.3 4.7 7.3 3.4 11.3-1.2 1.5-2 6 1.3 4.6 4-1.8 7.3-4.8 10.6-7.6 3-2 6.7-2.1 9.7-4 1.5-.3 4.4-3.1 5-1.6a45 45 0 0 1-7.4 12.3 32 32 0 0 1-18.8 10.9q-6.6 1.2-13.2.6"/>
|
||||||
|
<path d="M731.5 460.2q.4-4-1.7-8-3.3-6.2-8-11.9c-2.8-1.6-4.3 3.7-6.1 5.2-2.9 4.3-6.5 8.7-6.7 14-1.6 2.5-4.6-2-5.9-3.5a19 19 0 0 1-4-12 51 51 0 0 1 3.6-20.6c2-5.6 5.1-11 4.8-17 .2-4.7-.7-9.7-4.4-12.8-3.6-2.8 2.3-3.4 4.1-2 3.2.3 4.9 5.5 7.8 4.2 1.1-2.7 1.4-6 3.8-8.1 2.3-3.2 4.7 1.3 5.5 3.5 1.7 1.8 0 6.5 2.6 6.6 3.2-2.3 5.5-6 9.6-6.9 1.7-1 4.5 0 2.3 1.8-3 2.9-5.6 6.4-6.2 10.7-.9 5.3.4 10.7 2.7 15.4 4.5 9.4 8 20 5.7 30.5-1 4.6-4.2 8.6-8 11.3-.5.3-1.3.3-1.5-.4"/>
|
||||||
|
<path d="M726.7 389.6a21 21 0 0 0-5.6-7c-2.4 0-3.9 3-5.5 4.6-1.1 2.1-2.5 5.6-5.3 2.9-4.5-2.6-5.2-8.3-5.2-13-.3-7.6 2.8-14.7 5.5-21.6 1.7-4.3 1.3-9.2.2-13.6-1.3-5-5.4-8.6-8.5-12.6.2-1.5 4.2-.7 5.7-.4 3.4.9 5.4 3.8 7.9 6 1.8-.6 1-4.2 1.9-5.9 0-2.4 3.2-5.5 4.5-2.1 2 2.2 0 6.5 2.5 7.8 2.4-.9 3.6-3.5 5.8-4.7a8 8 0 0 1 7.8-.5c.9 2.2-2.6 4-3.6 6a20 20 0 0 0-3.8 18c1.4 5 3.8 9.5 4.7 14.5a40 40 0 0 1-.5 17.2c-.9 3.4-3.8 5.6-6.8 7q-1-1.1-1.7-2.6"/>
|
||||||
|
<path d="M711.6 326.9c-3.4-2.5-4.5-4.8-4.5-9.5 0-2.3.5-3.6 2-5.8q3.6-4.7-1.3-3.3-7.8 2.3-8-4.3c0-2.2.4-3.1 3.3-6.7q3.6-4.1 2.8-4.8-.6-.7-9 7.8a124 124 0 0 1-11.4 10.6c-9.8 6.6-19.2 7.6-23.5 2.5-2.2-2.6-2.1-4 .4-5.6a27 27 0 0 0 4.4-3.7 86 86 0 0 1 16.1-11.6q5.5-2.9 2.1-3c-3 0-12.5 6.2-19.8 12.8-2.1 2-5.2 4.2-6.8 5a25 25 0 0 1-13.9 1c-2.2-.7-6.3-4.5-6.3-5.9 0-.3 1-1.1 2-1.8a30 30 0 0 0 4.6-3.2c5.8-5 16.8-10.3 25.5-12.2 2.8-.5 1.7-2-1.4-1.8a56 56 0 0 0-25 11.7c-8.3 6.9-20.8 6.2-24.8-1.3-.7-1.3-1.2-2.5-1-2.7a93 93 0 0 0 20.4-7.8 52 52 0 0 1 18.1-6.5c2.8-.5 3-1.9.3-2.2-3.6-.4-9 1.4-18.5 6-12.3 6.1-15.8 7.2-22.2 6.8-6-.4-9.3-1.9-14-6.4-3.2-3-7.6-10.5-6.8-11.4a64 64 0 0 0 15.8 1.3c8.3 0 10.6-.2 15-1.5a84 84 0 0 0 24-12.1 58 58 0 0 1 36.8-13.6c12.4 0 20.2 2.8 27.2 9.9 2.4 2.4 4.4 3.9 4.7 3.6s.6-4.5.7-9.3c0 0 3.7-.4 4.5.7 0 7.7 0 8.4 1.2 8.4q1.2-.1 2-2c1-2.5 5-6 9.2-8.2 9-4.5 24.7-4.7 37.3-.3a62 62 0 0 1 16.7 9.5 90 90 0 0 0 24 12c6.8 2 19 2.5 25.1 1l5.4-1c2.3 0-1.6 7.6-6.2 12.1-8.4 8.2-19.3 8.1-34.6-.1-9.6-5.2-21-8-21-5.2q0 1 1.5 1 5 0 18.7 6.5a54 54 0 0 0 18.3 6.5q3.5 0 .2 4.7-3.5 5-11.7 5c-5.3 0-8.3-1.1-13-5-8-6.6-27.6-14-26.9-10q.2 1 3.2 1.5a56 56 0 0 1 23.1 11l5.9 4.3c1.1.6 1.1.8.2 2.5-1.4 2.8-5.2 4.9-9.2 5.3q-7.6 1-14.5-5c-10-8.3-19.3-14.3-22.3-14.3q-3.7.1 3 3.7a80 80 0 0 1 15.8 11c2 1.9 4.3 3.7 5 4.1 1.9 1 1.8 2.4-.2 5s-5.4 3.8-9.7 3.3c-8.6-.9-15.4-5-26-16a71 71 0 0 0-8.2-7.8q-2 .2 2.2 5c3.4 3.7 4 5.8 2.7 9-1.1 2.6-3 3.3-6.8 2.2q-6-1.5-2 3.1c3.8 4.9 3.3 10.7-1.5 14.8a12 12 0 0 1-3.4 2.3q-.8-.1-2.3-2.4-4.3-6.9-8.7 0l-2 3z"/>
|
||||||
|
<path d="m726.7 233-5.2 4-4.6-3.4v27.8h9.8z"/>
|
||||||
|
<path d="M694.9 204.3a88 88 0 0 1-9 32.3l11.1-10.3 7.7 9.2 8.4-9.4 8.5 8 8.2-8.3 8.5 10 7.4-8.2 12.6 9c-4.6-10-10.7-18.6-10-32.8-12.1 9-41 10.6-53.4.4z"/>
|
||||||
|
<path d="M717 197.6q-7-.2-13.4 1a20 20 0 0 0-7.8 3c.3 8.6 41 12.1 51.9.2a20 20 0 0 0-8.2-3.3c-4-.8-8.6-.8-12.9-1v7.1H717z"/>
|
||||||
|
<path d="M724.9 154h-6.3v49.4h6.4z"/>
|
||||||
|
<path d="m724.9 155.2-2.4 23.7 24.3 11.9-12.3-16.5 16.8-5.5zm-2.7-6.1c-3.7 0-6.4 1.4-6.4 3s2.7 3 6.4 3 6.4-1.4 6.4-3-2.7-3-6.4-3"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#f7e017">
|
||||||
|
<path d="M314 375.9q3.8-1.1 5.3-5.6c.5-1.6.9-3.2.8-3.6-.2-1-1.4-1-2.6.1-.9.7-1 1.1-.8 2.6.7 3.7-.7 4.7-7.7 5.4-.7 0-2.8 0-4.5-.3q-5.1-.6-3.4 1l2.1.9c1.9.5 8.8.2 10.8-.5m14.7-.6c.4-.4 1.7-1 3-1.5q2.4-.6 3.3-2.2c2.1-3 1.7-5.7-1.3-9.3-1.7-2-2.4-1.9-3.7.3-1.2 1.8-1.1 2 .5 2.4q1.4.4 2.1 1.6 2.7 4.8-1.4 5c-2.4 0-3 .4-3.7 2l-.6 1.9c0 .6 1 .5 1.8-.2m-4.8-3.5c.4-1.3.6-3.5.5-8a33 33 0 0 0-.3-6.4c-.4-.4-2.3.8-2.6 1.7a3 3 0 0 0 .5 2.1c.6 1.2.7 2.4.5 7q-.5 8.9 1.4 3.6"/>
|
||||||
|
<path d="m312.6 369 .7-5c.1-1.7.5-3.8.7-4.7q1-3.5-1.8-1.6l-1.3 1 .2 3.3c.3 3-.2 8.5-.9 10.7q-.3.9.9-.5c.6-.7 1.3-2.2 1.5-3.3zm-10 1.6c2.4-2 2.1-5.6 2.7-8.4 0-1.9 1.2-4.1.4-5.8-2.3.4-3.7 2.6-2.5 4.7 0 2.5 0 5.2-1.3 7.4-1 1.5-4.4 1.1-4.2-1 .8-3-2.9-1.5-4.3-.7-1.1.8-3.5.9-2.6-1-.6-2.7-3.9-1-5.7-1-1.7 0-.1-3.5-2.6-3-4.5-.3-9.5.1-13.5-2.6-2.3-1.1-2-3.9-.7-5.7 1.4-2.4 1.8-5.5 4-7.4 2.3-2.1-2-1.2-3-.5-2.2 1.2-.2 4.3-2 6-1 1.8-2.4 4.2-4.8 3.9-3.5-.7-5.5-4-8-6-2.2-.5-1 3.4.2 4.2a22 22 0 0 0 7.4 3.6c2.6-.5 2.7 3 5 3.5 4 2 8.6 2.5 13.1 2.8 1.8.1.8 3.3 3.1 2.6 1.3.4 4.3-.5 4.4 1-2 2.4 1.9 2.3 3.3 2 1.9-.4 4.2-1 4.7 1.4 1.5 1.7 4.3 1.4 6.2.5z"/>
|
||||||
|
<path d="M262.8 350.4a24 24 0 0 0 2.4-4.2 16 16 0 0 1 2-3.6q1.6-2-1.4-1.6-1.4.2-1.5 1.5a23 23 0 0 1-2.5 7c-1.7 2.5-1.7 2.6-1 2.6q.6-.2 2-1.7m-25-15.7c-1.9 0-2 1.2-.2 1.8q1.4.4 2.3 2.3c1.7 3.5 2.8 4.2 7.5 4.6l3 .2.2 1.9q.1 1.7.5 1.8l2.6-1c2.2-1.2 4.3-3.8 4.3-5.5 0-1-1.8-2.2-3.4-2.2-.7 0-2 .6-3.1 1.4-3.4 2.4-7 2-9-1.2q-2.3-4-4.7-4zm16.3 6.5q1.3 0 .6 2a.9.9 0 0 1-1.7 0c-.4-1 .1-2 1-2zm126.5-4c-1.3 0-1.6.2-2.4 1.4-1.3 1.9-1.4 6-.2 7.4.7.9.8.9 2.3.2 2.2-.9 2.6-.8 2.5.3 0 3-4.2 8.7-8.6 11.7a10 10 0 0 0-2.4 2c-.3.8 1.3.7 3.3-.4a21 21 0 0 0 7.9-8c1.1-2.3 1.3-3 1.5-7 0-3.8 0-5-.6-6.2-.8-1.4-1-1.5-2.8-1.5h-.5zm.1 2.5c1 0 1 .2 1.2 1.6q.1 1.4-.7 2.3c-.8.7-1 .7-1.6-.4-1-1.6-.4-3.5 1.1-3.5m-20.2 28.5c3.9-2 6.2-4.1 7.6-7.2l1.3-3.1c0-.6-1.9-1.5-3-1.5s-1.4-.8-1-3c.5-2.1 0-4.8-1-4.8q-.7.1-1.3 1.1c-.6 1-.7 1.4-.2 2.7q1 3-1.7 4.9-1.3 1-1.3 2l.1 1 2.1-1 2-1.2 1.1 1q1.1.8 1.2 1.7c0 2.4-6.8 6.4-11.4 6.8-2.5.2-3 0-3.8-.8q-1-1-.7-2l.5-2.6q.5-3-2 .6c-1.2 2-1.6 4.1-.9 5.2.6 1 4.4 1.8 7.2 1.6q2.5 0 5.2-1.4m26-14.5c2.4-2.5 3.5-5.5 3.5-10v-3.5l2-1c2.7-1.2 5.2-3.7 5.2-5.1q0-2.1-1.8.2c-.9 1.1-2 1.8-6 3.7-1 .4-1.1.7-1.4 5-.2 5-1 6.8-3.7 10.2-1.7 2-1.8 2.4-.6 2.4.5 0 1.8-.9 2.8-2zm-26.7-2.8c.2-.7-1.2-1.2-1.7-.6q-.5.4-.2.9c.4.6 1.6.4 1.9-.3m36.8-9.5c.3-.8-1.1-1.3-1.7-.7q-.4.5-.1 1c.4.6 1.6.4 1.8-.3m-44.3-25.9q-1.4 1-2.1 2.3c-.5.3-.1.6.1 1 1.7 1.7 2.4 4.2 3.2 6.5.8 2.7 1.8 5.6 1 8.4-.3 1-1.2 2.1-2.4 1.8-2-.1-4-.7-6-.7-1.9.1-3.3 1.8-5.1 1.6-1.2 0-1.2-2.4-2.2-1.7-.6 1.3-.3 2.7-.4 4q.5.3 1.1.2h3.7c.2 1.2.2 2.7 1 3.7q2 .4 4-.5c1.2-.6 1.4-2.1 1.8-3.3.4-1.3 2-1 3-1.5a6 6 0 0 0 4-5.7c-.2-3.9-1.6-7.4-2.8-11l-1.5-4.9zm-6 21.8c1.3 0 1.9 1.6 1.6 2.7-.5 1.5-2.4.6-2.7-.5-.7-1-.3-2.3 1-2.2z"/>
|
||||||
|
<path d="M296 324.8q-1 0-2 .7c-3.5 2.5-4.5 5.4-2 6.6q2.6 1.1-1.5 3.2c-4 2-7.5 1.7-14.2-1q-2.3-1-1.7 1c.4 1.5 1.8 2.3 5.1 3 3.6 1 8 .7 10.8-.5a14 14 0 0 0 4.3-3.4l2.2-2.3 2.5.3c3.1.4 3.2.4 3.2 1.9 0 1.2 0 1.2 2.9 1.5l4.7.2q1.9 0 2.4.9c.6.7.9.8 5.6.4 4.4-.4 5.2-.4 7.2.3q2.3.6 4.1.5c3.4-.4 8-3.1 8.7-5.1 0-.3 1.3-.7 2.7-1q5-1.1.4-1.8a23 23 0 0 1-4.6-1.1 12 12 0 0 0-3.5-.9c-1.7 0-3.3 1-3.3 2.2 0 .7.2.8 2.3.6 1.8-.2 2.4-.1 3.4.7q1.1.9 1 1.3c-.5.8-4.5 2.6-6.2 2.9a5 5 0 0 1-3-.5c-1.6-.8-3.8-.9-4.3-.2q-.3.4-1.3-.5l-1-1-2.4 1q-3.4 1.6-3.3-.2c0-.5-.7-.6-4.2-.3-3.9.2-4.3.1-5-.7q-1-.9-.2-1.7c.4-.8.4-1 0-1.5q-.4-.6-2.5 0c-3.9 1-5 .5-5-2.5q-.2-3-2.3-3m-1 2.8q.3 0 .7.4.5.6.3 1.3c-.3.9-2 .9-2.3 0q-.3-.8.5-1.3z"/>
|
||||||
|
<path d="M288 330.4c2.4-1.5 2.4-1.4 2.7-5.5.2-3 .2-3.2-.6-3.2q-1.8 0-1.8 3.7c0 1.6-.2 2.3-1 3-2 2-6.8 1.1-7.5-1.3q-.3-1 1.1-3c2.1-3 1.7-3.8-1-1.5-1.7 1.6-2 1.6-1.7.3q.4-2-1.8-1.4-1.1.2-1.3 1.6-.3 1.2-1.3 1.5c-1.2.3-3.2-.8-3.2-1.8 0-.7 3-4.4 6.9-8.4 1.4-1.5 2.6-3 2.6-3.1q-.2-.4-1.7-.4-2 0-1.8.8c0 .4-1.9 3-4.3 5.7-5 5.6-5.4 6.7-3 8.2a6 6 0 0 0 6.6-.2l1.6-1.1v2c0 2.5.5 3.5 2.5 4.5a8 8 0 0 0 8-.4m104.4-34.6c-1.8 1.1-.4 3.4 0 5-.8 2-3.5 2.6-5.5 3-2.8.5-4.8 2.8-5.8 5.3-.6 1.6-2 4-3.5 1.6-1.3-1.3-3.7-2.4-5.2-.8-1.2 1.1-1.5 2.7-2 4.2-.7-1.1-1-2.8-2.4-3.2-2.4.3-1.5 3.3-.4 4.5 1 1.5 2 3.3 1 5-1 2-4 3.4-5.7 1.7-1.6-.9-.5-4-2.2-4.2-.8.6-.8 3.9-2.1 2.1-1-1.5-.4-3.6-1.6-4.9-1.3.2-2.4 2.5-2 3.7 1.8 2.4 2.6 5.4 3.3 8.3.4 1.2-.1 3.5 1 4 .7-1.9 0-4 .6-5.9 1.8-.2 3.7.6 5.5.2 2.7-.3 4.7-2.6 5.6-4.9q.3-2.7-.1-5.4c2 .4 4.2.4 6.2 1 1 1.5-.3 3.7-.6 5.3-1 3.4-3.7 5.8-6.2 8-1.1.7-1.2 2.4.3 1.5a15 15 0 0 0 7.5-8c1.1-2.6.2-5.5 1-8.1 1-2 3.5-1.6 5.4-1.6s3.5-2.3 2.9-4.2c-.6-2.2 1.7-3.2 3.2-4 2.1-1 3.7-3.1 3.5-5.5 0-1.3 0-3.6-1.7-3.7m-7.3 12.5c2.2.6-.4 4.8-1.6 2.1-.4-1 .5-2.1 1.6-2.1m-10.3 3c2.9-.1 1.8 4-.6 2.2-1.3-.7-.9-2.2.6-2.2M270 327.6q0-.6-.6-.7c-.7 0-1.2.7-.9 1.3.4.7 1.3.3 1.5-.6m34-3.6q0-1-.8-.8c-1.1.2-1.3 1.7-.1 1.7q.9 0 .9-1zm-42-20.4c-1.3-.3-2.2.9-2.7 2q-1.4 2.8-4 4.3-2 .4-3.9-.8c-1.3-.7-1-2.3-1.6-3.4-1-.8-2.7.3-2.6 1.5-.1 1.6 1.3 2.5 2.6 3.1 1 .7 2.6 1 3 2.3 0 1.1.4 2.4 1.7 2 1.5 0 2 1.8 1.3 2.9a6 6 0 0 0-.7 4c.7.7 1.4-1 2-1.4l1-1.4q4 .4 8 .4c2 0 3.5-1.2 4.7-2.6 1.8-1.8 3.2-3.9 5.1-5.4 1.4-.4.7-3-.8-2.2-1.3.5-1.7 2-2.6 2.9a31 31 0 0 1-5 5.2c-1.5.6-3.1.3-4.6 0-.6-.5 1.2-1 1.5-1.6q1.4-1.1 2.3-2.7c-.5-1-1.9-1-2.9-1-2.4.2-4.3 2.3-6.8 2.5-1.9 0-.9-2 0-2.7q2.5-3 5.1-5.7c.5-.6 2.3-1.2 1.2-2q-.6-.2-1.3-.2m1.2 10c1.3.7-.8 1.8-1.6 1.7-1.1.3-1.2-.8-.2-1q.9-.6 1.8-.7m-3.8 2.6c.7 0 2.2.7.8 1.1-1 .8-2-.8-.8-1.1"/>
|
||||||
|
<path d="M289.4 317.8c0-1-1.6-.8-1.8.2q-.3.8.8.6 1-.1 1-.8m74.7-6.6c.2-.9-1-1.5-1.7-.8s0 1.9.8 1.7q.6-.1 1-.9zM248 302.1c1.1-1 1.2-1.1.7-3.3-.8-3.1-.7-3.5.5-3.8 1.5-.3 5.3 1.7 6 3.3.8 1.3.7 1.4-.4 2.4-1.2 1.1-1.2 2.4 0 2.4 1 0 3.7-2.6 3.7-3.5 0-1.3-3-4.4-5.4-5.5a11 11 0 0 0-4.6-1c-3.1 0-3.5.7-2.7 4.2q1.4 6.4-3.7 1.6a10 10 0 0 1-3.5-8.6q0-6 5.1-6.6 3.5-.6 0-1.2c-3.6-.6-6.6 1.8-7.7 6-1.3 4.7 1.6 10.4 6.7 13.3 2.7 1.5 3.7 1.6 5.3.3m139.2-5.2q.5-.5.5-1.4.1-1.3 1.1-2.4 1-1.4 1-1.8c0-.8-1.3-.8-2.3 0q-1.8 1.4-2 0l1.2-.9q2.4-1.1.4-2.1c-1.7-.8-3.5.6-3.6 3-.1 1.6 0 1.8 1.2 2.5s1.3 1 1.1 2q-.3 2.6 1.4 1.1m13-1.4q1.6-1.6 1.8-2.2t1.9-2c2.8-2 3.5-4 2.2-7.3-.5-1.3-2-3-5.5-6a26 26 0 0 0-5.4-4.4c-.9 0-.7 3.4.2 3.7 1.7.6 2.8 1.3 5.4 3.7 3.2 2.8 4.6 5.5 3.8 7-.7 1.4-1.7 1-4.5-2a14 14 0 0 0-3.2-2.9q-.5.1-.5 1.4-.1 1.4 2 3.5c2.3 2.6 2.5 4.1.7 5.5l-1.4 1a33 33 0 0 0-9-10q-.5 0-.5 1.4c0 1.3.2 1.7 1.2 2.2a38 38 0 0 1 7 7l1.8 2zm6-16.8c-.5-1.2-8.4-9.4-9.3-9.4q-.6 0-.4 1.8c0 1.6.3 1.8 1.4 2.1a20 20 0 0 1 4.6 3.7 17 17 0 0 0 3.7 3zm-47.8 92.6a1.2 1 0 1 1-2.3 0 1.2 1 0 1 1 2.3 0m4.2-1.4a1.2 1 0 1 1-2.4 0 1.2 1 0 1 1 2.4 0"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,673 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bo" viewBox="0 0 640 480">
|
||||||
|
<path fill="#007934" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#ffe000" d="M0 0h640v320H0z"/>
|
||||||
|
<path fill="#d52b1e" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m368.6 210.7-98 97.9-1.3-1 98-97.8 1.3 1z"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M374.7 204.5c-.4.7-1.4 2.5-1 3.8l-2-1.5q.7 1.4-.1 1.8c-.3.4-1.4.3-2.1.2q1 .7 2.4 1h2c-.7.3-2.2.8-3.3 1-.5.1-1.6.2-2 0-.6.5-1.9-.4-1.3-1q-.4-.5-.4-1.4-.1-1.2.7-3l.5 1.7q.4.8 1.1 1.4-.5-.8.1-1.8.9-.7 2-.2l-1.9-1.3c.8 0 3.3-1 4.1-1.5l4.9-3.8z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m387 222.8-125.7 70.9-.9-1.2 125.7-71 1 1.3z"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M394.9 218.3c-.6.6-2.2 2-2.2 3.4l-1.4-2q.2 1.4-.7 1.7c-.4.4-1.4 0-2-.3a5 5 0 0 0 2 1.5l1.7.6c-.6.1-2.3.3-3.4.2-.5 0-1.5-.2-1.9-.6-.7.4-1.6-.8-1-1.2l.2-1.4q.3-1.2 1.6-2.8v1.8q.1.7.5 1.6-.2-1 .7-1.7 1-.5 2 .3l-1.4-1.7c.8.1 3.5-.2 4.4-.5l6-2.5c-1.7 1-4.6 3-5.1 3.6z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m376.3 217.5-113 85.2-1-1.1 112.9-85.2 1.1 1z"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M383.4 212.1c-.5.6-1.8 2.3-1.6 3.6L380 214q.4 1.4-.4 1.7c-.4.4-1.5.2-2.1 0q.9.8 2.2 1.2l1.9.3c-.7.3-2.3.6-3.4.6-.5 0-1.6 0-2-.3-.6.5-1.7-.6-1.1-1.1q-.2-.5-.1-1.4a6 6 0 0 1 1.1-3l.2 1.8q.3.8.9 1.6-.4-.9.4-1.8 1-.5 2 0l-1.6-1.5c.7 0 3.3-.6 4.2-1s3.8-2 5.5-3.1c-1.4 1.2-4 3.5-4.4 4z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m271.4 210.7 98 97.9 1.3-1-98-97.8-1.3 1z"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M265.3 204.5c.4.7 1.4 2.5 1 3.8l2-1.5q-.7 1.4.1 1.8c.3.4 1.4.3 2.1.2q-1 .7-2.4 1h-2c.7.3 2.2.8 3.3 1 .5.1 1.6.2 2 0 .6.5 1.9-.4 1.3-1q.4-.5.4-1.4.1-1.2-.7-3l-.5 1.7q-.4.8-1.1 1.4.5-.8-.1-1.8-.9-.7-2-.2l1.9-1.3c-.8 0-3.3-1-4.1-1.5l-4.9-3.8z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m253 222.8 125.7 70.9c.2-.3.6-1 .9-1.2l-125.7-71-1 1.3z"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M245.1 218.3c.6.6 2.2 2 2.2 3.4l1.4-2q-.2 1.4.7 1.7c.4.4 1.4 0 2-.3a5 5 0 0 1-2 1.5l-1.8.6c.7.1 2.3.3 3.4.2.6 0 1.6-.2 2-.6.7.4 1.6-.8 1-1.2l-.2-1.4a6 6 0 0 0-1.6-2.8v1.8q-.1.7-.5 1.6.2-1-.7-1.7-1-.5-2 .3l1.4-1.7a17 17 0 0 1-4.4-.5l-6-2.5c1.7 1 4.6 3 5.1 3.6z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m263.7 217.5 113 85.2 1-1.1-112.9-85.2-1.1 1z"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M256.6 212.1c.5.6 1.8 2.3 1.6 3.6l1.7-1.7q-.4 1.4.4 1.7c.4.4 1.5.2 2.1 0a5 5 0 0 1-2.2 1.2l-1.9.3c.7.3 2.3.6 3.4.6.5 0 1.6 0 2-.3.6.5 1.7-.6 1.1-1.1q.2-.5.1-1.4a6 6 0 0 0-1.1-3l-.2 1.8q-.3.8-.9 1.6.4-.9-.4-1.8-1-.5-2 0l1.6-1.5a15 15 0 0 1-4.2-1c-1-.4-3.8-2-5.5-3.1 1.4 1.2 4 3.5 4.4 4z"/>
|
||||||
|
<path fill="#00e519" stroke="#000" stroke-width=".1" d="M300.1 283.4c4-2.6 15.1-4 16.7-3.6-8 6-16 6.3-16.7 3.7z"/>
|
||||||
|
<path fill="#ffe533" stroke="#000" stroke-width=".1" d="M300.2 283.5c.7 2.6 8.7 2.4 16.6-3.6a69 69 0 0 1-16.6 3.6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".1" d="M300.2 283.5c.7 2.6 8.7 2.4 16.6-3.6a69 69 0 0 1-16.6 3.6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M300.1 283.4a41 41 0 0 1 16.7-3.6c-8 6-16 6.3-16.7 3.6z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="M347.6 220.2 322 272.5c-1.3 1-2.3-.3-2.7-.2-1.5 1.7-3.6 2-4 2.5-1.8 2.4-.8 4.3-.7 4.5 1.3 1.8-1.6 3.5-1.5 4-.6 1-2.7.9-3.1 2l-4.8 9.3c-.6.5-3.7 6.1-3.7 6.1-2 0-10.2-5.2-10.4-5.1 4.6-7.3 15.6-18.5 15.3-19.2 3.1-5.2 8-10.9 10.1-10.8 3-1.6 4.5-5.7 4-6.6 2.2 0 3.5-1.4 3.6-1.5l18.8-37.6c1.6-.5 1.4.1 1.9 1 0 0 1-1.2.9-1.3.9-.4 1.8.2 1.8.6z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M348.6 208.8c-.6 1 .2 1 .5 1.1l1 .3c1.3 0 2 .7 2 1l-30 61.3c-1.3 1-2.5-.3-2.9-.1l20.6-41.8 10-18.8-2.8-1.2q-1.2-.4-.7-1.6l12.6-21.6-10.2 21-.1.4"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M348.6 208.8c-.6 1 .2 1 .5 1.1l1 .3c1.3 0 2 .7 2 1l-30 61.3c-1.3 1-2.5-.3-2.9-.1l20.6-41.8 10-18.8-2.8-1.2q-1.2-.4-.7-1.6l12.6-21.6-10.2 21-.1.4"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M308.8 272.4c-3 0-4.6 2-2.7 4.7m1.4-2.4c-1 .6-1.7-.3-1.7-.3m15.2-13c-1.5 5.6-4.3 9.3-5 10.4-2 2.2-3.9 7.2-3.5 8.1l-8.1 13.3"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M322 272.7c-1.4-.5-5.7-4.5-10-3.8-3.3 3.8-5.3 7.7-5.9 8.2q6.1 4.9 7.6 5.4c.7-.4 1-1.5 1-1.5.9-1-.2-1.7-.2-1.7.2-2.5 2-4.2 3.8-4.3 2.2-.2 1.6-.4 1.9-.4 1-.6 1.8-1.9 1.8-1.9z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M322 272.7c-1.4-.5-5.7-4.5-10-3.8-3.3 3.8-5.3 7.7-5.9 8.2q6.1 4.9 7.6 5.4c.7-.4 1-1.5 1-1.5.9-1-.2-1.7-.2-1.7.2-2.5 2-4.2 3.8-4.3 2.2-.2 1.6-.4 1.9-.4 1-.6 1.8-1.9 1.8-1.9z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M317.6 272.8c-2 0-4 .9-4.8 2.6l4.8-2.6"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M317.6 272.8c-2 0-4 .9-4.8 2.6m-3.5.7q.3.6 1.1.5.9-.4.6-1-.4-.8-1.2-.5t-.5 1zm2.5-3.4q.3.6 1.2.4.8-.3.5-1t-1.2-.5q-.8.4-.5 1z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M345.3 226.1q1.3-.5.8-2l-4.7-1.7s-.6 0-1 .7.1 1.2.1 1.2l4.8 1.8"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M345.3 226.1q1.3-.5.8-2l-4.7-1.7s-.6 0-1 .7.1 1.2.1 1.2l4.8 1.8"/>
|
||||||
|
<path fill="#00e519" stroke="#000" stroke-width=".1" d="M294.5 286c3.9-2.7 15-4.2 16.6-3.8-7.8 6.2-15.8 6.5-16.6 3.9z"/>
|
||||||
|
<path fill="#ffe533" stroke="#000" stroke-width=".1" d="M294.6 286c.7 2.7 8.7 2.3 16.5-3.8a62 62 0 0 1-16.5 3.9z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".1" d="M294.6 286c.7 2.7 8.7 2.3 16.5-3.8a62 62 0 0 1-16.5 3.9z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M294.6 286a40 40 0 0 1 16.6-3.9c-7.9 6.2-16 6.6-16.6 4z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m340.7 222-24.4 52.8c-1.3 1-2.4-.3-2.7-.2-1.5 1.7-3.6 2.1-4 2.5-1.8 2.5-.7 4.4-.6 4.6 1.3 1.8-1.5 3.5-1.4 4-.6 1-2.7.9-3.1 2.1-.1-.1-4.2 8.4-4.6 9.3-.6.5-3.5 6.2-3.5 6.2-2.1 0-10.3-5-10.5-5 4.4-7.3 15.1-18.7 14.9-19.4 3-5.3 7.7-11 9.9-11 3-1.6 4.3-5.7 3.7-6.6 2.3-.1 3.5-1.5 3.7-1.6l18-37.8c1.6-.6 1.3 0 1.9 1 0 0 1-1.2.9-1.4.9-.4 1.7.2 1.8.6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m340.7 222-24.4 52.8c-1.3 1-2.4-.3-2.7-.2-1.5 1.7-3.6 2.1-4 2.5-1.8 2.5-.7 4.4-.6 4.6 1.3 1.8-1.5 3.5-1.4 4-.6 1-2.7.9-3.1 2.1-.1-.1-4.2 8.4-4.6 9.3-.6.5-3.5 6.2-3.5 6.2-2.1 0-10.3-5-10.5-5 4.4-7.3 15.1-18.7 14.9-19.4 3-5.3 7.7-11 9.9-11 3-1.6 4.3-5.7 3.7-6.6 2.3-.1 3.5-1.5 3.7-1.6l18-37.8c1.6-.6 1.3 0 1.9 1 0 0 1-1.2.9-1.4.9-.4 1.7.2 1.8.6z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M341.4 210.6c-.5 1 .3 1 .6 1.2l1 .3q2 .2 2 1l-28.7 61.7c-1.3 1-2.5-.3-2.9 0l19.7-42.2 9.6-19-2.8-1q-1.2-.4-.7-1.7l12-21.8-9.6 21.1-.2.4"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M341.4 210.6c-.5 1 .3 1 .6 1.2l1 .3q2 .2 2 1l-28.7 61.7c-1.3 1-2.5-.3-2.9 0l19.7-42.2 9.6-19-2.8-1q-1.2-.4-.7-1.7l12-21.8-9.6 21.1-.2.4"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M303 275c-3-.2-4.6 2-2.6 4.6m1.3-2.4c-1 .6-1.7-.3-1.7-.3m15-13.3c-1.5 5.7-4.2 9.4-4.7 10.6-2.2 2.2-3.9 7.3-3.5 8.1l-7.8 13.4"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M316.1 275c-1.3-.5-5.8-4.5-10-3.7-3.3 3.9-5.1 7.8-5.7 8.3a50 50 0 0 0 7.7 5.3c.7-.4 1-1.5 1-1.5.8-1-.2-1.8-.2-1.8 0-2.4 1.8-4.2 3.6-4.3 2.2-.2 1.6-.4 1.9-.4 1-.6 1.7-2 1.7-2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M316.1 275c-1.3-.5-5.8-4.5-10-3.7-3.3 3.9-5.1 7.8-5.7 8.3a50 50 0 0 0 7.7 5.3c.7-.4 1-1.5 1-1.5.8-1-.2-1.8-.2-1.8 0-2.4 1.8-4.2 3.6-4.3 2.2-.2 1.6-.4 1.9-.4 1-.6 1.7-2 1.7-2z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M311.8 275.1c-2 0-4 1-4.7 2.7l4.7-2.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M311.8 275.1c-2 0-4 1-4.7 2.7m-3.6.8q.4.6 1.2.4.8-.4.5-1t-1.2-.5q-.8.4-.5 1zm2.4-3.5q.4.6 1.3.5.8-.4.5-1-.4-.8-1.2-.5t-.6 1z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M338.5 228q1.3-.5.7-2l-4.7-1.6s-.5 0-1 .7.2 1.2.2 1.2l4.8 1.8"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M338.5 228q1.3-.5.7-2l-4.7-1.6s-.5 0-1 .7.2 1.2.2 1.2l4.8 1.8"/>
|
||||||
|
<path fill="#00e519" stroke="#000" stroke-width=".1" d="M340.6 283.3a39 39 0 0 0-16.8-3.7c8 6.1 16.1 6.3 16.8 3.7z"/>
|
||||||
|
<path fill="#ffe533" stroke="#000" stroke-width=".1" d="M340.6 283.3c-.7 2.7-8.8 2.4-16.8-3.6a63 63 0 0 0 16.8 3.6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".1" d="M340.6 283.3c-.7 2.7-8.8 2.4-16.8-3.6a63 63 0 0 0 16.8 3.6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M340.6 283.3c-4.2-3-15.8-4-16.9-3.7 8 6 16.2 6.3 16.9 3.6z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m292.7 219.3 25.7 52.9c1.3 1 2.4-.3 2.7-.2 1.6 1.7 3.7 2.1 4.2 2.5 1.8 2.4.8 4.4.7 4.6-1.4 1.8 1.5 3.5 1.4 4.1.6 1 2.7.8 3.2 2l4.8 9.3c.7.5 3.7 6.2 3.7 6.2 2.1 0 10.3-5.3 10.5-5.2-4.6-7.3-15.7-18.7-15.4-19.4-3.1-5.2-8.1-11-10.2-10.9-3-1.6-4.5-5.7-4-6.6-2.3 0-3.6-1.5-3.7-1.6l-19-37.9c-1.6-.5-1.3.1-1.9 1 0 0-1-1.2-.9-1.3-.9-.5-1.8.2-1.8.5z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M291.7 207.8c.5 1-.2 1-.5 1.2l-1.1.3c-1.2 0-1.9.7-1.9 1l30.2 61.9c1.3 1 2.5-.3 3-.1l-20.8-42.3-10-19 2.7-1.1q1.2-.3.7-1.6l-12.7-21.9 10.2 21.2.2.4"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M291.7 207.8c.5 1-.2 1-.5 1.2l-1.1.3c-1.2 0-1.9.7-1.9 1l30.2 61.9c1.3 1 2.5-.3 3-.1l-20.8-42.3-10-19 2.7-1.1q1.2-.3.7-1.6l-12.7-21.9 10.2 21.2.2.4"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M331.9 272.1c3 0 4.6 2.1 2.7 4.7m-1.4-2.3c1 .6 1.7-.4 1.7-.4M319.5 261c1.6 5.7 4.4 9.5 5 10.6 2.2 2.2 4 7.3 3.7 8.2l8.1 13.4"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M318.6 272.4c1.3-.4 5.7-4.6 10-3.8 3.4 3.8 5.4 7.7 6 8.2q-6.1 5-7.7 5.5c-.7-.4-1-1.5-1-1.5-.9-1 .2-1.8.2-1.8-.1-2.4-2-4.2-3.8-4.3-2.3-.2-1.6-.4-2-.4-1-.6-1.7-1.9-1.7-1.9z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M318.6 272.4c1.3-.4 5.7-4.6 10-3.8 3.4 3.8 5.4 7.7 6 8.2q-6.1 5-7.7 5.5c-.7-.4-1-1.5-1-1.5-.9-1 .2-1.8.2-1.8-.1-2.4-2-4.2-3.8-4.3-2.3-.2-1.6-.4-2-.4-1-.6-1.7-1.9-1.7-1.9z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M323 272.5c2 0 4 1 4.8 2.7l-4.8-2.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M323 272.5c2 0 4 1 4.8 2.7m3.6.6a1 1 0 0 1-1.2.5.7.7 0 0 1-.5-1q.3-.7 1.2-.5.8.4.5 1zm-2.5-3.4q-.4.6-1.3.5-.7-.4-.5-1 .4-.8 1.2-.5t.6 1z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M295 225.4a1.4 1.4 0 0 1-.8-2l4.8-1.8s.5.1 1 .7-.2 1.2-.2 1.2l-4.8 1.9"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M295 225.4a1.4 1.4 0 0 1-.8-2l4.8-1.8s.5.1 1 .7-.2 1.2-.2 1.2l-4.8 1.9"/>
|
||||||
|
<path fill="#00e519" stroke="#000" stroke-width=".1" d="M345.6 286a38 38 0 0 0-16.6-3.8c7.9 6.2 15.9 6.5 16.6 3.9z"/>
|
||||||
|
<path fill="#ffe533" stroke="#000" stroke-width=".1" d="M345.6 286c-.8 2.7-8.7 2.3-16.5-3.8a62 62 0 0 0 16.5 3.9z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".1" d="M345.6 286c-.8 2.7-8.7 2.3-16.5-3.8a62 62 0 0 0 16.5 3.9z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M345.6 286a40 40 0 0 0-16.6-3.9c7.8 6.2 15.9 6.6 16.6 4z"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m299.5 222 24.4 52.8c1.3 1 2.3-.3 2.7-.2 1.5 1.7 3.6 2.1 4 2.5 1.7 2.5.7 4.4.6 4.6-1.4 1.7 1.5 3.5 1.4 4 .5 1 2.6.9 3 2.1.2-.1 4.2 8.4 4.7 9.3.6.5 3.5 6.2 3.5 6.2 2 0 10.3-5 10.5-5-4.4-7.4-15.2-18.7-15-19.4-2.9-5.3-7.7-11-9.8-11-3-1.6-4.3-5.7-3.7-6.6-2.3-.1-3.6-1.5-3.7-1.6L304 222c-1.6-.6-1.4 0-2 1 0 0-.9-1.2-.8-1.4-.9-.4-1.8.2-1.8.6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m299.5 222 24.4 52.8c1.3 1 2.3-.3 2.7-.2 1.5 1.7 3.6 2.1 4 2.5 1.7 2.5.7 4.4.6 4.6-1.4 1.7 1.5 3.5 1.4 4 .5 1 2.6.9 3 2.1.2-.1 4.2 8.4 4.7 9.3.6.5 3.5 6.2 3.5 6.2 2 0 10.3-5 10.5-5-4.4-7.4-15.2-18.7-15-19.4-2.9-5.3-7.7-11-9.8-11-3-1.6-4.3-5.7-3.7-6.6-2.3-.1-3.6-1.5-3.7-1.6L304 222c-1.6-.6-1.4 0-2 1 0 0-.9-1.2-.8-1.4-.9-.4-1.8.2-1.8.6z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M298.8 210.6c.4 1-.3 1-.6 1.2l-1 .3q-2 .2-2 1l28.7 61.7c1.3 1 2.5-.3 2.9 0L307 232.5l-9.6-19 2.7-1q1.4-.4.8-1.7L288.8 189l9.7 21.1.2.4"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M298.8 210.6c.4 1-.3 1-.6 1.2l-1 .3q-2 .2-2 1l28.7 61.7c1.3 1 2.5-.3 2.9 0L307 232.5l-9.6-19 2.7-1q1.4-.4.8-1.7L288.8 189l9.7 21.1.2.4"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M337.2 275c3-.2 4.5 2 2.6 4.6m-1.4-2.4c1 .6 1.8-.3 1.8-.3m-15-13.3c1.4 5.7 4.2 9.4 4.7 10.6 2.1 2.2 3.8 7.3 3.5 8.1l7.8 13.4"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M324 275c1.4-.5 5.8-4.5 10.1-3.7 3.2 3.9 5.1 7.8 5.7 8.3a54 54 0 0 1-7.8 5.3c-.6-.4-1-1.5-1-1.5-.7-1 .3-1.8.3-1.8 0-2.4-1.8-4.2-3.6-4.3-2.3-.2-1.6-.4-2-.4-1-.6-1.7-2-1.7-2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M324 275c1.4-.5 5.8-4.5 10.1-3.7 3.2 3.9 5.1 7.8 5.7 8.3a54 54 0 0 1-7.8 5.3c-.6-.4-1-1.5-1-1.5-.7-1 .3-1.8.3-1.8 0-2.4-1.8-4.2-3.6-4.3-2.3-.2-1.6-.4-2-.4-1-.6-1.7-2-1.7-2z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M328.4 275.1c2 0 4 1 4.7 2.7l-4.7-2.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M328.4 275.1c2 0 4 1 4.7 2.7m3.6.8a1 1 0 0 1-1.2.4q-.8-.4-.6-1 .4-.7 1.3-.5.7.4.5 1zm-2.5-3.5q-.4.6-1.2.5-.8-.4-.5-1 .4-.8 1.2-.5t.5 1z"/>
|
||||||
|
<path fill="#cce5e5" stroke="#000" stroke-width=".1" d="M301.6 228q-1.2-.5-.7-2l4.7-1.6s.6 0 1 .7-.2 1.2-.2 1.2l-4.8 1.8"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M301.6 228q-1.2-.5-.7-2l4.7-1.6s.6 0 1 .7-.2 1.2-.2 1.2l-4.8 1.8"/>
|
||||||
|
<path fill="#a05a2c" stroke="#000" stroke-width=".1" d="m315.3 250.7 35.5-38-1.4-.9-35.6 38z"/>
|
||||||
|
<path fill="#e7e7e7" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M349.8 212.2c-1.2-1-3.4-2.2-5-1.8-.5-2.2 2.5-4.4 3.8-4-.3 2.3 3.2 3.8 3.1 3.8z"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M352.1 210.4c1 .8 1.4.8 2.8 1.6 1.4.7 3.2-1.1 4.1-1.7 0 0 1 3.4-1.1 5.6-2 2.3-4.6 2.8-6.5 2.1 0 0 2.6-2.5 1.5-3.6-1-1.1-1.4-1.2-2.5-1.9"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M-27.7-406.5h4.1v2.2h-4.1z" transform="matrix(-.67726 .73575 -.82314 -.56784 0 0)"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m308.2 290.3-1 9 1.8-2.2c.3-.6 1.5-2.1 1.8-7.4 0 0-1-2.9-1.4-2.9-.7-.4-1.2 3.5-1.2 3.5zm2.2-20.1-2.7 15.8c0 .5 1.3 1.6 2.1-1.2l1.5-10.5z"/>
|
||||||
|
<path fill="#d52b1e" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m315.9 284.6-1.4-2-.4 3.1s2 1.5 1.7 4.3l.4-.6.2-1s.5-2 .5-3.1z"/>
|
||||||
|
<path fill="#ffe000" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M307.3 299.4s3.3-3 3.5-9.7l.4-1.9s0 1.5.8.4c.6-1.5.6-2.8.6-2.8s1.2-1.6 1.6.3l-1.2 9.6-.3 2s-.7-1-1.4.1-1.8 2.7-4 2zm4-25.1-1.5 10.5s1.2.6 1.4 3c.1 1.2.6.6.8.4.2-.8 0-2.3 0-2.3l.6-7.3s-1.2-3.4-1.3-4.3z"/>
|
||||||
|
<path fill="#ffe000" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m310.1 298.8-.2 2.6s1 0 1.7-1.2c.8-1.1 1-2.8 1-2.8s-.7-1.1-1.3 0c-.5 1-1.2 1.4-1.2 1.4zm1.8-10.6c.6-1 .8-2.6.7-2.8l-.7.5c.2 1.2 0 2.3 0 2.3z"/>
|
||||||
|
<path fill="#d52b1e" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M315.3 287.1a4 4 0 0 0-1.3-1.5l-1.5 12s-.4 3-2.3 3.7c0 0 1.1 10.3 4.5 7.5.4-.4 1-3.6.9-5.5l-.9-6a27 27 0 0 1 .5-4.7l.6-2.6c.2-.2 0-1.8-.5-2.9z"/>
|
||||||
|
<path fill="#d52b1e" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m316.9 285.3 1 1c.3 0-2.3 19.3-2.3 19.3s0-2-.7-6c-.6-3.2.2-7.5.9-9.6 0 0 .8-.5 1-4.7z"/>
|
||||||
|
<path fill="#f7e214" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m312.6 278.8-.7 7.1s1.8-2.4 2.2-.2l.4-3.2s-1.5-2.4-2-3.7z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M311.3 287.8s-.6-6.1-3.6-1.8a10 10 0 0 0 0 3c0 .8.9 1.8 1.2 2.3.7 1 1.3-.2 1.3-.2s.7-1 1-3.3z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M287.8 237c-.1-3.5-1.2-9.3-1.3-13.3l-12-12.2s-1.4 9.9-5.8 15.5l19 10"/>
|
||||||
|
<path fill="#ffe000" stroke="#000" stroke-width=".1" d="M288.7 237.3c.5-2.4 1-5 1.5-10.2l-7.7-7.6c0 3.2-3.6 8.1-3.9 14"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M295.3 244.7c1-4.3-1.3-4.8 2-10.8l-7.2-6.8c-1.3 4-2.2 6.6-2 10l6 4.4"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M267.6 270c-1.5-4.2.5-12.7.2-18-.1-3.5 2.5-16.7 2.4-20.6l-14.1-8.8s-.6 14.3-2.3 29.9q-2 12.1-.4 21.5c1.5 8.7 3 12.2 6.5 15.9 6.3 6.5 19.7 2.7 19.7 2.7 11.3-2.3 17.8-9.4 17.8-9.4s-3.7.8-9.6 1.4c-13-.9-18.2 2.4-18.6-11"/>
|
||||||
|
<path fill="#f7e214" stroke="#000" stroke-width=".1" d="m305.4 278.8.2-.1-5.9 1.9-8 .6c-17.2.4-15-10.3-14.3-27.5.1-6.6 1.4-14.8 1-17.7l-11.5-6.6c-3.8 10.6-2.6 18-3.3 23.5-.4 6-1.7 17.4.2 22.4 2.7 11.6 11.8 11.2 24.2 10.1 6.1-.5 9.4-2 9.4-2l8-4.6"/>
|
||||||
|
<path fill="#007a3d" stroke="#000" stroke-width=".1" d="M305.6 278.5a34 34 0 0 1-6 2.2l-8 .8c-12.5 1-19.8-7.7-18.2-27.6 0-7 .2-10.2 2.6-19.5l7 4v.7c-.5 2-1.5 7-1.5 9.2 0 16 10.1 28.2 23.9 30.3h.2"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M260 230.6c1 2 8 12.5 12 14.5m-11-7.6c1.3 2 9.8 14 13.6 14.6m-15.5 3.6c2 2.4 4 7 9.7 10m-7 3.3c4 3.7 13.6 11.6 23 12m-23-6c2 2.3 6.2 13.3 24 8.3m-26.7-6.3c1.2 2.6 10.1 17.3 26 11.6"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M277.8 264.8c-1.5-4.3.5-12.7.2-18.1-.1-3.5 1.5-15.5 1.5-19.5l-13.2-9.9s-.6 14.3-2.3 30a84 84 0 0 0-1.5 23.9c1.8 10 6.6 12.8 7.5 13.6 6.7 6 22 5.4 23.5 4.9 10.8-4 15.6-10.8 15.6-10.8s-5.2-.1-11.1.5c-13-1-19.7-.4-20.1-13.8"/>
|
||||||
|
<path fill="#ffe000" stroke="#000" stroke-width=".1" d="M315.6 273.5h.2l-5.9 1.8-8 .6c-17.2.5-15-10.2-14.3-27.5.1-6.5.3-12.4 0-15.3l-10.3-7.6c-3.8 10.7-2.8 16.7-3.5 22.2-.4 5.8-1.7 17.3.2 22.3 2.7 11.7 11.8 11.2 24.2 10.2 6.1-.5 9.4-2.1 9.4-2.1l8-4.6"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M315.9 273.4c-2.4 1.2-6 2.1-6 2.1l-8 .8c-12.5 1-19.8-7.7-18.2-27.6 0-7-.3-7.5 2.2-16.9 3.8 2.5 11.1 8.8 11.1 8.8s-2 2.8-1.5 6.7c0 16 6.4 24 20.2 26.1l1.5-13"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M270.1 225.3c1.2 2 8.1 12.6 12 14.6m-10.8-7.7c1.2 2 9.7 14 13.5 14.7m-15.5 3.6c2 2.3 4 7 9.7 10m-7 3.3c4 3.6 13.6 11.6 23 12m-23-6c2 2.3 6.2 13.2 24 8.3m-26.7-6.4c1.2 2.7 10.1 17.3 26 11.7"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="m256.2 224-.7 6q-.2 6.6.1 9.8c0 .2.9 5.5.6 5.8-1 1.1-1.1 1.2-2.2.4-.1-.2.5-5.6.5-6.3l.4-9.9c0-1 1-6.4 1-6.4s0-1.2.3.5"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m256.2 224-.7 6q-.2 6.6.1 9.8l.7 5.8c-1 1.1-1.2 1.6-2.3.7-.1-.2.5-5.9.5-6.6l.4-9.9c0-1 1-6.4 1-6.4s0-1.2.3.5z"/>
|
||||||
|
<path fill="#f7e214" stroke="#000" stroke-width=".1" d="M256 222.2s-1 6-1.2 9.6l-.3 7.9-.5 4.5c-.1.7.1.2 0 .2-.9.5-1.5.1-2-.3-.2-.1 1.4-3.8 1.4-4.5.9-10.8 2.4-17 2.4-17s-.6 3.7.3-.4"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="M256 222.2s-1 6-1.2 9.6l-.3 7.9-.5 4.5c-.1.7.1.2 0 .2-.9.5-1.5.1-2-.3-.1-.1 1.4-3.8 1.5-4.5.8-10.8 2.3-17.1 2.3-17.1s-.6 3.8.3-.3zm-.4 17.3s-1 .4-1 .2m0-1.3s.7 0 .8-.2m0-1s-.6.3-.8 0m.8-1.6h-.6m.6-1.5h-.6m.5-2.1s-.3.1-.4-.1m.5-1.7h-.5m-.5 9.5s-.9.2-1-.1m1.1-2s-.9.1-1-.1m1-1.3h-.7m.9-1.5h-.7m.7-1.7h-.5m.7-1.5h-.6m.6-1.7s-.4.3-.4 0m0 9s-.9 0-.9-.3m12.8-19.8-.7 6a63 63 0 0 0 0 9.8c0 .3 1 5.5.7 5.8-1 1.2-1.1 1.3-2.2.4-.1-.1.5-5.5.5-6.3l.4-9.8c0-1.1 1-6.5 1-6.5s0-1.1.3.6"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m266.4 218.7-.7 6a73 73 0 0 0 .1 9.8l.7 5.8c-1 1.2-1.2 1.6-2.3.7-.1-.1.5-5.8.5-6.6l.4-9.8c0-1.1 1-6.5 1-6.5s0-1.1.3.6z"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="M266.3 217s-1.2 6-1.3 9.5l-.3 8-.5 4.4v.3c-.9.5-1.5 0-2-.4-.2 0 1.4-3.7 1.4-4.5a127 127 0 0 1 2.4-17l.3-.4"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M266.3 217s-1.2 6-1.3 9.5l-.3 8-.5 4.4v.3c-.9.5-1.5 0-2-.4-.2 0 1.4-3.7 1.4-4.5.9-10.8 2.4-17 2.4-17zm-.5 17.2s-1 .5-1 .2m0-1.2s.7 0 .8-.3m0-1s-.6.4-.8.1m.7-1.7h-.5m.6-1.5h-.6m.5-2s-.4 0-.4-.2m.5-1.7h-.5m-.5 9.6s-.9.1-1-.2m1.1-1.9s-.9 0-1-.2m1-1.3h-.7m.9-1.4h-.7m.7-1.7-.5-.1m.7-1.4-.6-.1m.6-1.6s-.4.2-.4 0m0 9s-.9 0-.9-.4"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="M274.3 211.3s.5 5.1.2 8c-.3 3.5-.3 4.5-.6 6.6v3.9c.8.5 1.5.2 2 0 .3-.1-1-3.3-1-4 .5-8.9-.4-14.3-.4-14.3l-.2-.2"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="M274.3 211.3s.5 5.1.2 8c-.3 3.5-.3 4.5-.6 6.6v3.8c0 .5-.1 0 0 .1.8.5 1.4.2 2 0 .2-.1-1-3.3-1-4 .5-9-.4-14.3-.4-14.3zm-.3 14.6s.9.2 1 0m-.8-1.7s.9.2 1 0m-1-1.2h.9m-.8-1.2h.7m-.5-1.4h.5m-.5-1.3h.5m-.4-1.4s.4.2.4 0m-1 7.4s1 .1 1-.1"/>
|
||||||
|
<path fill="#005000" stroke="#000" stroke-width=".1" d="M306 221.7h.8z"/>
|
||||||
|
<path fill="#fff" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M316.7 256.4s-.3-.2-.4 0l.1.2zm-1 1.1 2.1-.1"/>
|
||||||
|
<path fill="#e8a30e" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M292.2 284.2c-.2 3-7.4 6.6-12.7.1-5.7-4.5-4.5-11.4 0-12.3l54.7-53.3c2.2-1.2 2.4-2.3 3.5-3.5 2.3 2.5 7 6.8 9.6 9q-2.6 1.9-3.2 3.5z"/>
|
||||||
|
<path fill="#e7e7e7" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width=".1" d="M337.8 215c2.6-3.5 12.8 5.8 10 8.7-2.6 2.8-12.4-5-10-8.6z"/>
|
||||||
|
<path fill="#cccccf" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M347 223c-2 1.4-9.3-4.8-8.1-7.2 2-2.2 10.1 5.7 8 7.2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M344 227.9a16 16 0 0 1-9.9-9m-23 44.8a16 16 0 0 1-11.4-11.6m9.1 14.3a16 16 0 0 1-11.4-11.6m-2.1 25.8c-5.8-1.8-10.4-6-12.2-12.1m9.8 14.8a18 18 0 0 1-12.3-12.2"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".1" d="M279.4 285q-.4 2-2.1 1.4m13.7-2c-2.1 3.5-4.5 2.4-6.5 2.5"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="M276.4 285.2q.2 1.6 1.7 1.6a2 2 0 0 0 1.7-1.6q-.1-1.6-1.7-1.6c-1.6 0-1.7 1-1.7 1.9"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".1" d="M279.5 284.9q-.5 2-2 1.3m13.5-1.7c-2.1 3.4-4.5 2.3-6.5 2.4"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m331.8 290.3 1 9-1.8-2.2c-.3-.6-1.5-2.1-1.8-7.4 0 0 1-2.9 1.4-2.9.7-.4 1.2 3.5 1.2 3.5zm-2.2-20.1 2.7 15.8c0 .5-1.3 1.6-2.1-1.2l-1.5-10.5z"/>
|
||||||
|
<path fill="#d52b1e" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m324.1 284.6 1.4-2 .4 3.1s-2 1.5-1.7 4.3l-.4-.6-.2-1s-.5-2-.5-3.1z"/>
|
||||||
|
<path fill="#ffe000" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M332.8 299.4s-3.4-3-3.6-9.7l-.4-1.9s0 1.5-.8.4c-.6-1.5-.6-2.8-.6-2.8s-1.2-1.6-1.6.3l1.2 9.6.3 2s.7-1 1.4.1 1.8 2.7 4 2zm-4.1-25.1 1.5 10.5s-1.2.6-1.4 3c-.1 1.2-.6.6-.8.4-.2-.8 0-2.3 0-2.3l-.6-7.3s1.2-3.4 1.3-4.3z"/>
|
||||||
|
<path fill="#ffe000" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m329.9 298.8.2 2.6s-1 0-1.7-1.2c-.8-1.1-1-2.8-1-2.8s.7-1.1 1.3 0c.5 1 1.2 1.4 1.2 1.4zm-1.8-10.6a6 6 0 0 1-.7-2.8l.7.5c-.2 1.2 0 2.3 0 2.3z"/>
|
||||||
|
<path fill="#d52b1e" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M324.8 287.1a4 4 0 0 1 1.2-1.5l1.5 12s.4 3 2.3 3.7c0 0-1.1 10.3-4.5 7.5-.4-.4-1-3.6-.9-5.5l.9-6a27 27 0 0 0-.5-4.7l-.6-2.6c-.2-.2 0-1.8.5-2.9z"/>
|
||||||
|
<path fill="#d52b1e" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m323.1 285.3-1 1c-.3 0 2.3 19.3 2.3 19.3s0-2 .7-6c.6-3.2-.2-7.5-.9-9.6 0 0-.8-.5-1-4.7z"/>
|
||||||
|
<path fill="#f7e214" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m327.4 278.8.7 7.1s-1.8-2.4-2.2-.2l-.4-3.2s1.5-2.4 2-3.7z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M328.7 287.8s.6-6.1 3.6-1.8c0 0 .2 2.4 0 3 0 .8-.9 1.8-1.2 2.3-.7 1-1.3-.2-1.3-.2s-.7-1-1-3.3z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M352.2 237c.1-3.5 1.2-9.3 1.3-13.3l12-12.2s1.4 9.9 5.8 15.5l-19 10"/>
|
||||||
|
<path fill="#ffe000" stroke="#000" stroke-width=".1" d="M351.3 237.3c-.5-2.4-1-5-1.5-10.2l7.7-7.6c0 3.2 3.6 8.1 3.9 14"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M344.7 244.7c-1-4.3 1.3-4.8-2-10.8l7.2-6.8c1.3 4 2.2 6.6 2 10l-6 4.4"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M372.4 270c1.5-4.2-.5-12.7-.2-18 .1-3.5-2.5-16.7-2.4-20.6l14.1-8.8s.6 14.3 2.3 29.9q2 12.1.4 21.5c-1.5 8.7-3 12.2-6.5 15.9-6.3 6.5-19.7 2.7-19.7 2.7-11.3-2.3-17.8-9.4-17.8-9.4s3.7.8 9.6 1.4c13-.9 18.2 2.4 18.6-11"/>
|
||||||
|
<path fill="#f7e214" stroke="#000" stroke-width=".1" d="m334.6 278.8-.2-.1 5.9 1.9 8 .6c17.2.4 15-10.3 14.3-27.5-.1-6.6-1.4-14.8-1-17.7l11.5-6.6c3.8 10.6 2.6 18 3.3 23.5.4 6 1.7 17.4-.2 22.4-2.7 11.6-11.8 11.2-24.2 10.1-6.1-.5-9.4-2-9.4-2l-8-4.6"/>
|
||||||
|
<path fill="#007a3d" stroke="#000" stroke-width=".1" d="M334.4 278.5a34 34 0 0 0 6 2.2l8 .8c12.5 1 19.8-7.7 18.2-27.6 0-7-.2-10.2-2.6-19.5l-7 4v.7c.5 2 1.5 7 1.5 9.2 0 16-10.1 28.2-23.9 30.3h-.2"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M380 230.6c-1 2-8 12.5-12 14.5m11-7.6c-1.3 2-9.8 14-13.6 14.6m15.5 3.6c-2 2.4-4 7-9.7 10m7 3.3c-4 3.7-13.6 11.6-23 12m23-6c-2 2.3-6.2 13.3-24 8.3m26.7-6.3c-1.2 2.6-10.1 17.3-26 11.6"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M362.3 264.8c1.4-4.3-.6-12.7-.4-18.1.2-3.5-1.4-15.5-1.4-19.5l13.2-9.9s.6 14.3 2.3 30a84 84 0 0 1 1.5 23.9c-1.8 10-6.6 12.8-7.5 13.6-6.7 6-22 5.4-23.5 4.9-10.8-4-15.6-10.8-15.6-10.8s5.2-.1 11.1.5c13-1 19.7-.4 20.1-13.8"/>
|
||||||
|
<path fill="#ffe000" stroke="#000" stroke-width=".1" d="M324.4 273.5h-.2l5.9 1.8 8 .6c17.2.5 15-10.2 14.3-27.5-.1-6.5-.3-12.4 0-15.3l10.3-7.6c3.8 10.7 2.8 16.7 3.5 22.2.4 5.8 1.7 17.3-.2 22.3-2.7 11.7-11.8 11.2-24.2 10.2-6.1-.5-9.4-2.1-9.4-2.1l-8-4.6"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M324.1 273.4a34 34 0 0 0 6 2.1l8 .8c12.5 1 19.8-7.7 18.2-27.6 0-7 .3-7.5-2.2-16.9-3.8 2.5-11.1 8.8-11.1 8.8s2 2.8 1.5 6.7c0 16-6.4 24-20.2 26.1l-1.5-13"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M369.9 225.3c-1.2 2-8.1 12.6-12 14.6m10.8-7.7c-1.2 2-9.7 14-13.5 14.7m15.5 3.6c-2 2.3-4 7-9.7 10m7 3.3c-4 3.6-13.6 11.6-23 12m23-6c-2 2.3-6.2 13.2-24 8.3m26.7-6.4c-1.2 2.7-10.1 17.3-26 11.7"/>
|
||||||
|
<path fill="#f7e214" stroke="#000" stroke-width=".1" d="m383.8 224 .7 6q.3 6.6-.1 9.8c0 .2-.8 5.5-.6 5.8 1 1.1 1.1 1.2 2.2.4.1-.2-.5-5.6-.5-6.3l-.4-9.9c0-1-1-6.4-1-6.4s0-1.2-.3.5"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="m383.8 224 .7 6q.3 6.6-.1 9.8l-.7 5.8c1 1.1 1.2 1.6 2.3.7.1-.2-.5-5.9-.5-6.6l-.4-9.9c0-1-1-6.4-1-6.4s0-1.2-.3.5z"/>
|
||||||
|
<path fill="#f7e214" stroke="#000" stroke-width=".1" d="M384 222.2s1 6 1.2 9.6l.3 7.9.5 4.5c.1.7 0 .2 0 .2.9.5 1.5.1 2-.3.2-.1-1.4-3.8-1.4-4.5-.9-10.8-2.4-17-2.4-17s.6 3.7-.3-.4"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="M384 222.2s1 6 1.2 9.6l.3 7.9.5 4.5c.1.7-.1.2 0 .2.9.5 1.5.1 2-.3.2-.1-1.4-3.8-1.4-4.5-.9-10.8-2.4-17.1-2.4-17.1s.6 3.8-.3-.3zm.4 17.3s1 .4 1 .2m0-1.3s-.7 0-.8-.2m0-1s.6.3.8 0m-.7-1.6h.5m-.6-1.5h.6m-.5-2.1s.4.1.4-.1m-.5-1.7h.5m.5 9.5s.9.2 1-.1m-1.1-2s.9.1 1-.1m-1-1.3h.7m-.9-1.5h.7m-.7-1.7h.5m-.7-1.5h.6m-.6-1.7s.4.3.4 0m0 9s.9 0 .9-.3"/>
|
||||||
|
<path fill="#f7e214" stroke="#000" stroke-width=".1" d="m373.6 218.7.7 6a73 73 0 0 1-.1 9.8c0 .3-.8 5.5-.6 5.8 1 1.2 1.1 1.3 2.2.4.1-.1-.5-5.5-.5-6.3l-.4-9.8c0-1.1-1-6.5-1-6.5s0-1.1-.3.6"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="m373.6 218.7.7 6a73 73 0 0 1-.1 9.8l-.7 5.8c1 1.2 1.2 1.6 2.3.7.1-.1-.5-5.8-.5-6.6l-.4-9.8c0-1.1-1-6.5-1-6.5s0-1.1-.3.6z"/>
|
||||||
|
<path fill="#f7e214" stroke="#000" stroke-width=".1" d="M373.7 217s1.2 6 1.3 9.5l.3 8 .5 4.4v.3c.9.5 1.5 0 2-.4.2 0-1.4-3.7-1.4-4.5a127 127 0 0 0-2.4-17l-.3-.4"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="M373.7 217s1.2 6 1.3 9.5l.3 8 .5 4.4v.3c.9.5 1.5 0 2-.4.2 0-1.4-3.7-1.4-4.5-.9-10.8-2.4-17-2.4-17zm.5 17.2s1 .5 1 .2m0-1.2s-.7 0-.8-.3m0-1s.6.4.8.1m-.7-1.7h.5m-.6-1.5h.6m-.5-2s.4 0 .4-.2m-.5-1.7h.5m.5 9.6s.9.1 1-.2m-1.1-1.9s.9 0 1-.2m-1-1.3h.7m-.9-1.4h.7m-.7-1.7.5-.1m-.7-1.4.6-.1m-.6-1.6s.4.2.4 0m0 9s.9 0 .9-.4m-10.5-22s-.5 5.2-.2 8.1c.3 3.5.3 4.5.6 6.6v3.9c-.8.5-1.4.2-2 0-.2-.1 1-3.3 1-4-.5-8.9.4-14.3.4-14.3l.2-.2"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="M365.7 211.3s-.5 5.1-.2 8c.3 3.5.3 4.5.6 6.6v3.8c0 .5.1 0 0 .1-.8.5-1.4.2-2 0-.2-.1 1-3.3 1-4-.5-9 .4-14.3.4-14.3zm.3 14.6s-.9.2-1 0m.8-1.7s-.9.2-1 0m1-1.2h-.9m.8-1.2h-.7m.5-1.4h-.5m.5-1.3h-.5m.4-1.4s-.4.2-.4 0m1 7.4s-1 .1-1-.1"/>
|
||||||
|
<path fill="#005000" stroke="#000" stroke-width=".1" d="M334 221.7h-.8z"/>
|
||||||
|
<path fill="#fff" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M323.3 256.4s.3-.2.4 0l-.1.2zm1 1.1-2.1-.1"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M291.7 215.5c0-2.1 2-3.3 2.2-3.6 1-.6 1.7-1.2 3.7-1.5l.2.9c0 .3-.5 1.6-2 2.7a12 12 0 0 1-4.1 1.5z"/>
|
||||||
|
<path fill="#a05a2c" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m291.6 214.7 29.7 38.4 1.4-1.3-30.2-39.1z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M291.2 207.2s3.2-.4 2.8-2.2-2.6-1.8-3.5-1.9c-1 0-4 .7-4.8 1.5-.9 1-2.7 2.5-2.1 5s1.3 4.4 2.3 6 .7 3.2.4 3.9c0 .3-.4 1.3.4 1.6 1.2.5 1.5.5 2.5-.6s2.5-3 2.5-5c0-2.1 2-3.3 2.2-3.6 1-.6 1.7-1.2 3.7-1.5 0 0-.7-1.2-1.8-1-1.1 0-3.4-1-4.6-2.2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M291.2 207.2s3.2-.4 2.8-2.2-2.6-1.8-3.5-1.9c-1 0-4 .7-4.8 1.5-.9 1-2.7 2.5-2.1 5s1.3 4.4 2.3 6 .7 3.2.4 3.9c0 .3-.4 1.3.4 1.6 1.2.5 1.5.5 2.5-.6s2.5-3 2.5-5c0-2.1 2-3.3 2.2-3.6 1-.6 1.7-1.2 3.7-1.5 0 0-.7-1.2-1.8-1-1.1 0-3.4-1-4.6-2.2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M291.2 207.2c-.4 0-1.7-.6-2.6-.3-.9.4-2.7 1.4-2.4 3m10.4-.3s-1.8.8-3.1 1.7c-.6.3-2.4 2-3.5 3.2-1 1-1.3 2.4-3.5 3.9m9-9-1.4 1q-.9.6-1.2 1.3"/>
|
||||||
|
<path fill="#e8a30e" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M347.8 284.2c.2 3 7.4 6.6 12.7.1 5.7-4.5 4.5-11.4 0-12.3l-54.7-53.3c-2.2-1.2-2.4-2.3-3.5-3.5a133 133 0 0 1-9.6 9 10 10 0 0 1 3.2 3.5z"/>
|
||||||
|
<path fill="#e7e7e7" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width=".1" d="M302.2 215c-2.6-3.5-12.8 5.8-10 8.7 2.6 2.8 12.4-5 10-8.6z"/>
|
||||||
|
<path fill="#cccccf" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M293 223c2 1.4 9.3-4.8 8.1-7.2-2-2.2-10.1 5.7-8 7.2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M296 227.9a16 16 0 0 0 9.9-9m23 44.8q8.6-2.5 11.4-11.6m-9.1 14.3q8.7-2.6 11.4-11.6m2.1 25.8c5.8-1.8 10.4-6 12.2-12.1m-9.8 14.8a18 18 0 0 0 12.3-12.2"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".1" d="M360.6 285q.5 2 2.1 1.4m-13.7-2c2.1 3.5 4.5 2.4 6.5 2.5"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="M363.6 285.2q-.2 1.6-1.7 1.6a2 2 0 0 1-1.7-1.6q.1-1.6 1.7-1.6c1.6 0 1.7 1 1.7 1.9"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".1" d="M360.5 284.9q.5 2 2 1.3m-13.5-1.7c2.1 3.4 4.5 2.3 6.5 2.4"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M339.6 175.5c1.8 2.9 4.4 8 5.2 12a23 23 0 0 1-7 20.8c-5.2 4.7-13.3 6-16.7 6.8s-5.7 1.8-6.3 2.5q-.1-.7.5-1.6c1.6-.7 4.1-1 7.8-1.8 7.2-1.5 14.8-4.2 19-12.2 5.4-10.3 2.2-18.4-2.5-26.4z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M341.6 206.2a.4.6 49.9 0 1-.6-.6.4.6 49.9 1 1 .6.6z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M346.6 205q-1.7.7-3 1l-3.7 1.4c-.8.3-1.6 1.3-1.6 1.3s1.2 1.3 2.6 1.1q1.6-.2 2.3-.7c.6-.3.6-.6 1.4-1.2 1-.7 1.6-2 2-3zm-5.7 1.1q-.6.7-1.6.5l-.2.2q1 .1 2-.5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M346.6 205q-1.7 1.6-4.7 2.8a14 14 0 0 1-5 1l-.2.3c1.4-.1 3.4-.4 5-1.1a15 15 0 0 0 4.9-3zm-2.4 4.6c-2-.1-3 .5-4.8.9s-3.7-.5-4.8 1.1c4.4 2.9 7.6 1 9.6-2z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M340.2 213.6c-.8-.8-8-3.2-9.2-.5 1.7 2 6.8 2.4 9.2.5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m338.5 215.5-2.5-.4c-1-.1-1.3-.3-1.9-.4-1-.1-2.3-1.6-6-.5 1.4 3.4 6.4 4 10.4 1.3zm1.6-1.9c-3.8.8-8.3 0-10.1-1l-.3.2a16 16 0 0 0 10.4.8zm4-4c-2.2 1.5-5.2 2.6-11.4 1.8v.2c8.5.7 9.3-.8 11.4-2z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M338.5 215.5c-3 .1-4.7 1.4-10.3-1.3l-1.4-.6-.5.2 1.4.4c7 3 6.8 1.6 10.8 1.3z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M327.3 211.6a.4.6 66.2 1 0 .4.7.4.6 66.2 0 0-.4-.7z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M327.1 212.3c-.6.1-.8.9-.8 1.3l-.4.2q.2-1 1.2-1.7z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M346 186.8a.4.6 15.8 1 1-1-.2.4.6 15.8 0 1 1 .2z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M341.6 203.2c-.1-2.1-1.1.8-3.2-3.9-.6-1.4-.6-2.2-1-4.3 1.2 1.8 3 2.3 3.8 3.6s.5 3.5.4 4.6z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M337.5 195.2s1 2.4 2.4 4a8 8 0 0 1 1.8 3.7"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M338.5 197.2c1.3 1.7 3 3.8 3 6h.3c-.3-2.8-2-4.1-3-5.6zm10.8 2.7q-1.6 1-2.7 1.4l-1.4.9-1.4.5c-.7.5-2 2-2 2s1.3 1.1 2 1c2.4-.5 3.1-1.4 4.3-2.3 1-.8 1-2.5 1.2-3.5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m340.7 204.8-.2.4c1.3-.2 3.5-1.5 5-2.2 1.8-1 3-1.7 3.7-3a9 9 0 0 1-3.9 3c-1.5.7-3.7 2-4.6 1.8zm9.4-9.5q-1.3 1.2-2.4 1.8l-1.3 1.1-1.2.8c-.6.6-1.5 2.2-1.5 2.2s.7.7 1.4.4c2.4-.3 3.1-1.4 4-3.5q.9-1.4 1-2.8z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M341.5 199.4a.6.4 62 1 0 .8-.3.6.4 62 0 0-.8.3z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m342.5 201.6-.2.3c2.9-1.3 6.6-3.9 7.8-6.5a17 17 0 0 1-7.6 6.2zm8.3-10.7q-1.2 1.3-2.2 2l-1 1.1-1.2.9c-.5.6-1.2 2.2-1.2 2.2s.8.9 1.5.5l2.1-1.6c.5-.4.6-1.4 1.2-2.2q1-1.4.8-2.9z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M348.7 193.8q-2.1 2.6-4.6 3.8l-.2.4c2.6-1.5 3.7-2.8 4.9-4.1zm1-5.6q-.6 1.4-1.6 2l-.8 1.2-1 .9-.7 2.1s.6.6 1.2.2l1.8-1.6 1-2.2a4 4 0 0 0 .2-2.6zm-4.3-1.1-.5 1.6v-.4q0-.6.4-1.3z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M348.4 191a13 13 0 0 1-3.8 4v.3c2.2-1.6 3-3 3.8-4.2zm-5.6 10q-.2-.8-.6-1.4.4.7.4 1.6l.1-.3z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M341.9 181.3a.4.3 39.5 0 1-.5.5.4.3 39.5 0 1 .5-.5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m343.1 182.5-1.2-.8 1.3 1v-.2zm5.6.4c-1.2 2.4-3.9 4-2.8 7.5 2.8 2.6 3-4.5 2.8-7.5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M346.1 177.7c-.5 2.4-2.6 4.4-1.2 7.6 4 1 2-4.3 1.2-7.6z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M343.7 174c2 3.7 2.5 5.9.2 8.3 0 0-1.3-1.1-1.6-3.3-.2-1.8 1.2-4 1.4-5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M340.4 171.8c.5 2.4-1.5 3.5 1.4 6.5 2.1-2.4 1.1-3-1.4-6.5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M339.6 176c-3.3.3-2-2.6-3-5.3 2 1.5 4.7 2.1 3 5.3zm2.2 4.4c-1-4.4-4-2.6-5.6-4.6.9 2.9 2.1 4.8 5.6 4.6zm1.4 4.1q-4 0-6.2-4.4c2.7 1.2 5.7 1.4 6.2 4.4z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M343.6 188.3c-1-1.2-1-2-1.5-2.7a9 9 0 0 0-3-3.7c0 3 .4 6.5 4.5 6.4z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M344.2 191.6a20 20 0 0 0-5.6-5.7c1 2.2.6 6.3 5.6 5.7z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M343.7 196c-5-.1-4.5-4.8-4.5-7l2.8 3.5c.9 1 1.8 2.2 1.7 3.5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M343.1 198.8c-.4-1 0-1.4-1-2.3-1-1-2.8-2.7-3.7-4.6-.1 1.7-.2 4.4 1.2 5.3 1 .7 2 .9 3.5 1.6zm-4.3 7.3c-3.9-3-1.6-5.6-1.2-7.9 1 2.6 3.7 4.8 1.2 7.9zm1.6-29.2c-1.3-2.3-2-3.6-3.7-6 2 2.7 2.8 4.5 3.9 6.4"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M338.9 206.8c.1-3-.7-5.7-1.3-8.5.5 3 1.3 6 1 8.8zm4.6-7.4c-.4-1.8-4.1-3-5-7.5.7 4.4 4.5 5.5 4.8 8zm.8-2.5-.1.5c-.6-3-4-4.4-5-8.4 1.5 4.3 4.3 4.9 5.1 7.9zm.7-4.4c-1.7-2.6-4.1-3.6-6.4-6.7 2.1 3 4.8 4.4 6.4 7zm0-3.5q-3-1-6-7.1c1.4 3 3 5.8 6 7.6zm-1-4c-2.1-1.7-5-2.8-7-4.9 1.7 2 5 3.3 7.1 5.2v-.3zm-1.6-4.3c-2.2-1.5-4.4-2-6.2-4.8 1.5 2.8 3.8 3.5 6.3 5zm6.3 2.2c-.8 3.1-1.5 6.3-3.6 8.4v-.4c1-.5 2.5-3.7 3.6-8zm-2.6-5.1c-.1 3.2 0 6.5-1.7 8.3l-.1-.3c1.6-1.4 1.5-5 1.8-8zm-2.2-3.6c.5 2.8.6 5.3-.2 9.5l-.1-.3c.4-2.4 1-5 .3-9.2zm-3.4-2.3c1.1 2.4 2 4.8 1.2 7.4l-.2-.3c.9-2.4-.1-4.7-1-7zm-4.4 31.2c1.4 3.2-.4 5.6-2.5 7-1.6-4.7 1.8-4.2 2.5-7z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M336.2 203.1c.2 2-2 4.2-2.6 7.6l-.3.2c1-4 3-5.6 2.9-7.8z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M343.7 181.2a.3.4 1.9 1 1-.6-.1.3.4 1.9 0 1 .6.1z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M343.5 183v-1.6h-.2v1.7z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M342.8 180.9a.3.4 2 0 1-.5.2.3.4 2 0 1 .5-.2z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m343.2 182.3-.5-1.1.4 1.4v-.2z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M344.7 186.9a.4.3 80.5 1 1-.7 0 .4.3 80.5 0 1 .7 0z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m344.9 188.6-.4-1.4h-.1l.4 1.6v-.2z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M343.6 187.2a.4.3 57 1 1-.5.5.4.3 57 0 1 .5-.5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m344.8 188.6-1.2-1v.1l1.3 1.1z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M343.4 199.1a.3.4 12.7 1 1-.7-.2.3.4 12.7 0 1 .7.2z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M342.8 200.8q.3-.7.2-1.5l-.4 1.7z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M344 199.9a.3.4 50.5 1 0 .5.4.3.4 50.5 1 0-.5-.4z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m342.7 201 1.3-.8v.1l-1.3 1z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M340.7 205a.3.4 40.4 1 1-.4-.6.3.4 40.4 0 1 .4.5z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m339.5 206.2.9-1.3h-.1l-1 1.3z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M338.8 204.5a.5.6 10 0 0 1 .1.5.6 10 1 0-1 0z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M339.4 205c-.3.7 0 1 0 1.2l-.1.3q-.4-.5-.1-1.5zm-12.1 9.5q-1-.1-1.3-.8h-.3q.5.9 1.5 1v-.3z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M327.3 215.2a.6.4 9.5 0 1 .2-.9.6.4 9.5 1 1-.2.8z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M332.7 206a5.5 5.5 0 0 1-4 6.4c-.9-4 2.8-4 4-6.3z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M332.7 206.1a21 21 0 0 1-4.8 7.2h-.5c2.7-2.1 4.2-4.9 5.3-7.2z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M327.8 213.3a.5.4 9.8 1 1-.4-.7.5.5 9.8 0 1 .4.7z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m326 213.8 1.4-.7h-.1l-1.7.6h.3zm2.5-5c0 1.2-1 2.4-2 3.2s-1 1.2-2.2 1.6c-1.2-2.8 2.8-3.3 4.2-4.8z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M328.5 208.8c-1.3 2.5-3.5 3.8-4.7 5.4h-.1c1.6-2.2 3-2.7 4.8-5.4z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M300.4 175.5c-1.7 2.9-4.4 8-5.1 12a23 23 0 0 0 7 20.8c5.2 4.7 13.3 6 16.6 6.8s5.8 1.8 6.4 2.5q.1-.7-.5-1.6c-1.6-.7-4.2-1-7.8-1.8-7.2-1.5-14.9-4.2-19-12.2-5.4-10.3-2.2-18.4 2.5-26.4z"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M314.1 213.6c-.2.4-4 3.7-7.5 3.5-2.5-.2-2.9-.8-2.9-.8s-.2-.7 2-1.1 6.2-2 8.4-1.6z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M303.8 216.3c2.7.3 5.5-.8 7.7-1.6"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M311.2 208.7a11 11 0 0 1 4.6 3.7c.9 1.5.7 1.9.7 1.9s-.2.3-1.3-1c-1-1.2-3.4-3.3-4-4.6z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M316.5 214.2c-.9-1.7-2.5-3-3.7-4.2"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M300.3 175.8c-.1-.3.3-3.4 2-4.4 1.3-.7 1.8-.5 1.8-.5s.3.3-.5 1.3c-1 1-2.1 3.2-3.3 3.7z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M304 171c-1.4.7-2.3 2.2-3 3.4"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M299.4 177.4c-.2-.2-1-3-.2-4.7.5-1.3.9-1.4.9-1.4s.3 0 .1 1.3-.2 3.8-.8 4.8z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M300 171.4q-.7 2.1-.6 4.4"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M299.4 177.5c.2 0 2.7-.8 3.9-2.1q1-1.4.8-1.3c-.2.1-.2-.2-1.1.5-1 .8-3 2-3.6 2.9z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M304 174.1q-1.5 1.7-3.3 2.6"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M298.2 179.6a10 10 0 0 1-.9-5.2c.3-1.6.6-1.7.6-1.7s.4 0 .4 1.4c0 1.5.3 4.2-.1 5.5z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M298 172.7c-.5 1.7-.2 3.6 0 5"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M297.3 181.6c-.3-.2-2.3-3-2-5.2 0-1.7.5-2 .5-2s.3-.1.6 1.4c.3 1.6 1.2 4.3.9 5.8z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M295.7 174.5c-.2 1.8.5 3.7 1 5.2"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M296.2 184.1a10 10 0 0 1-2.4-4.7c-.2-1.5 0-1.8 0-1.8s.4-.1.8 1.3 1.6 3.9 1.6 5.2z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M293.9 177.7c.1 1.7 1 3.4 1.5 4.8"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M298.2 179.6c.3 0 3.2-.2 4.6-1.7 1-1 1-1.5 1-1.5s-.2-.3-1.4.4c-1.1.8-3.4 1.8-4.2 2.8z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M303.7 176.5c-1 1.2-2.7 1.9-4 2.5"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M297.3 181.6c.2 0 3.2 0 4.6-1.4 1.1-1 1-1.4 1-1.4s0-.3-1.2.3-3.6 1.5-4.4 2.5z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M303 178.9q-2 1.5-4.1 2.2"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M296.4 184.2c.2 0 3.4-.2 4.9-1.7 1-1.1 1-1.5 1-1.5s-.1-.4-1.4.3c-1.2.8-3.7 1.8-4.5 2.9z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M302.3 181q-2 1.9-4.3 2.5"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M296.8 183s-.3-.5-.3-1q.1-.9.2-.8l-.2-.2-.1.9v.3l-.5-.5-.1-.4h-.2l.3.7c.5.4.8 1.1.8 1.1"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295.6 180.3a.5.4 83.5 1 1 .1 1 .5.4 83.5 1 1-.1-1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M296.8 180.3a.4.5 19.3 1 1-.3 1 .4.5 19.3 1 1 .3-1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m296.7 183 1-.7q.4-.6.3-.7h.2l-.4.8-.2.2.6-.1.4-.2.1.1-.6.4-1.3.3"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M299.4 181.6a.4.5 45.6 1 0-.8.8.4.5 45.6 1 0 .8-.8z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M298.4 180.8a.4.5 19.8 1 0-.3 1 .4.5 19.8 1 0 .3-1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295.4 186.9c-.2-.2-2.4-2.6-2.8-4.8-.3-1.6 0-1.9 0-1.9s.3-.1.8 1.3 2 4 2 5.4z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M292.6 180.3c.2 1.8 1.2 3.5 2 4.9"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295.6 186.9c.3 0 3.5-.4 5-2 1-1.3 1-1.7 1-1.7s-.2-.4-1.4.5-3.8 2-4.6 3.2z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M301.5 183.2c-1 1.4-2.8 2.2-4.2 3"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295.2 189.6c.3 0 3.5-.9 5-2.6 1.1-1.3 1-1.7 1-1.7s-.1-.2-1.4.7c-1.2 1-3.8 2.5-4.6 3.6z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M301.2 185.4c-1.1 1.4-3 2.4-4.3 3.2"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295 189.6c-.3 0-3.3-2.2-3.8-4.5-.3-1.7 0-2 0-2s.5-.3 1.2 1.2c.7 1.4 2.5 3.8 2.7 5.3z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M291.3 183.2c.3 1.8 1.5 3.5 2.5 4.8"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295.2 193a11 11 0 0 0 4.6-3.6c.9-1.5.7-1.8.7-1.8s-.2-.3-1.3 1c-1 1.2-3.4 3.1-4 4.5z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M300.5 187.7q-1.7 2.4-3.8 4"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295 193.2c-.3 0-3.6-1.7-4.4-4-.7-1.6-.4-2-.4-2s.4-.4 1.3 1 3.1 3.5 3.5 5z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M290.3 187.2c.5 1.8 2 3.4 3.2 4.6"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295 191.6s-.4-.5-.5-1v-.9h-.3v.8l.2.4-.6-.4-.3-.5-.2.2.5.6c.6.2 1.1 1 1.1 1"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M293 189a.6.4 69.4 1 1 .4 1.2.6.4 69.4 1 1-.4-1.1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M294.3 188.7a.4.6 5.2 1 1 0 1.2.4.6 5.2 1 1 0-1.2z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295 191.6s.6-.5.8-1l.2-.8h.3l-.3.8-.1.4.6-.3.4-.4.1.2-.6.5c-.6.1-1.2.7-1.2.7"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M297.5 189.4a.4.6 31.5 1 0-.6 1 .4.6 31.5 1 0 .6-1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M296.3 188.9a.4.6 5.7 1 0-.1 1.1.4.6 5.7 1 0 0-1.1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295.4 196.2a12 12 0 0 1-5.1-4c-1-1.7-.8-2.1-.8-2.1s.2-.3 1.4 1c1.3 1.4 3.8 3.6 4.5 5.1z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M289.5 190.2c1 1.8 2.8 3.3 4.2 4.6"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295.6 196.2c.4 0 4-1.9 5-4.4.6-1.8.3-2.3.3-2.3s-.4-.3-1.4 1.2c-1 1.4-3.4 3.8-3.9 5.5z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M300.9 189.6c-.7 2-2.3 3.7-3.6 5"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M296.6 199.3c.3-.1 3.7-2.3 4.5-4.8.7-1.9.4-2.3.4-2.3s-.4-.3-1.3 1.3c-1 1.5-3.2 4-3.6 5.7z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M301.5 192.3c-.7 2-2.2 3.8-3.4 5.2"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M296.3 199.3c-.4 0-4.7-1.3-6.2-3.8-1-2-.9-2.5-.9-2.5s.4-.4 1.8 1c1.5 1.4 4.5 3.5 5.3 5.2z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M289.3 193.1c1.1 2 3.3 3.6 5 4.8"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M300.1 205.7c.3-.2 2.6-3 2.8-5.6.2-1.8-.2-2-.2-2s-.3-.2-.8 1.4c-.5 1.7-1.8 4.6-1.8 6.2z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M302.8 198.1c-.1 2-1 4-1.8 5.6"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M300 205.6c-.5.2-5.4 0-7.7-2.1-1.6-1.6-1.5-2.2-1.5-2.2s.2-.5 2.1.5 5.7 2.3 7 3.8z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M290.9 201.4c1.7 1.7 4.4 2.6 6.5 3.4"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M298.2 203c-.4.1-4.8-1-6.8-3.1-1.6-1.6-1.5-2.1-1.5-2.1s.2-.4 2 .8c1.6 1.2 5 3 6.3 4.5z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M290 197.9c1.6 1.7 4 3 5.8 4"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M298.4 203c.4-.2 3.6-2.9 4-5.6.2-2-.2-2.5-.2-2.5s-.5-.2-1.2 1.5c-.7 1.8-2.6 4.7-2.6 6.5z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M302.2 195c-.2 2.2-1.5 4.3-2.5 6"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="m297.4 201.5-1-.8-.5-.9h-.3l.8 1.2-.8-.1-.5-.3v.2l.7.4c.7 0 1.6.5 1.6.5"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M294.2 199.8a.7.5 45.9 1 1 1 1 .7.5 45.9 1 1-1-1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M295.4 198.8a.7.5 71.6 1 1 .4 1.3.7.5 71.6 1 1-.4-1.3z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M297.5 201.4s.4-.6.4-1.3l-.1-1h.2l.1 1.3.6-.5.2-.6.2.1-.4.8c-.6.5-1 1.4-1 1.4"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M299.1 198.2a.5.7 8 1 0-.2 1.3.5.7 8 1 0 .2-1.3z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M297.6 198.1a.7.5 72.2 1 0 .4 1.3.7.5 72.2 1 0-.4-1.3z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M301.8 207.6c-.3.2-4.3 1-6.8-.3-1.8-.9-2-1.4-2-1.4s0-.5 2 0c1.8.4 5.2.7 6.8 1.7z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M293.1 206c2 1 4.4 1.2 6.3 1.4"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M302 207.5c.2-.1 2-2.5 1.9-4.8 0-1.7-.4-2-.4-2s-.3-.2-.6 1.3-1 4-.9 5.5z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M303.5 200.8c.2 1.8-.4 3.6-.9 5"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M305.5 210.3c-.3.3-5 1.5-8 .3-2-1-2.1-1.5-2.1-1.5s0-.5 2.2-.1c2.1.3 6.2.4 8 1.3z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M295.5 209.2c2.2 1 5 1 7.3 1.1"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M305.8 210.3c.2-.3 1.2-3.8 0-5.6-1-1.4-1.5-1.3-1.5-1.3s-.4 0 0 1.5c.3 1.5.5 4.3 1.5 5.4z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M304.4 203.4c1 1.4 1.1 3.4 1.3 5"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M303.3 208.7s-.5.4-1.2 0q-.9-.6-.8-.7l-.3.2 1 .7.3.2-.9.2-.6-.1v.3h1l1.5-.5"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M299.5 208.7a.8.5 24.1 1 1 1.4.7.8.5 24.1 1 1-1.4-.7z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M300.3 207.2a.8.5 49.9 1 1 1 1.2.8.5 49.9 1 1-1-1.2z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M303.4 209s.6-.5.2-1.2l-.6-.9.1-.2.7 1 .2.3.2-.9v-.6h.2v1l-.5 1.5"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M303.7 205.1a.8.5 69.5 1 0 .6 1.5.8.5 69.5 1 0-.6-1.5zm-1.5.8a.8.5 43.7 1 0 1 1 .8.5 43.7 1 0-1-1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M308.3 211.8c-.3.4-4.2 3-7.4 2.3-2.3-.4-2.6-1-2.6-1s-.1-.6 2-.8c2.2-.1 6-1 8-.5z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M298.4 213.1c2.5.6 5.2 0 7.3-.5"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M310.8 212.6s-.5.7-1.3.5l-1.2-.5-.3.3 1.3.4.5.1-.9.6-.7.1v.3l1.2-.3c.2 0 1.4-1 1.4-1"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M306.6 214a.9.6 5.4 1 1 1.7.2.9.6 5.4 1 1-1.7-.1zm.4-2a.9.6 31.2 1 1 1.5 1 .9.6 31.2 1 1-1.5-1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M309.5 212.2c.2-.3.7-4-1-5.6-1.1-1.2-1.7-1-1.7-1s-.5.1.1 1.5 1.4 4.2 2.6 5.1z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M306.8 205.6c1.4 1.2 1.9 3.2 2.3 4.7"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M311.4 213s.2-.7-.5-1.2q-1-.7-1.1-.6v-.4l1.2.7.4.3c.4.3 0-.8-.2-1l-.4-.7.3-.2.5 1 .2 2"/>
|
||||||
|
<path fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M309.7 209a.9.6 42.7 1 0 1.3 1.1.9.6 42.7 1 0-1.3-1.2zm-1.2 1.5a.9.6 17 1 0 1.7.6.9.6 17 1 0-1.7-.6z" overflow="visible" style="marker:none"/>
|
||||||
|
<path fill="#452c25" d="M319.1 209s-2.3 5.4-1.5 6c0 0 2.4-4.3 4.4-5.8 1.1-1 1.8 0 2-1 0-.8-3-2.1-3-2.1l-1.8 2.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".2" d="M319.1 209s-2.3 5.4-1.5 6c0 0 2.4-4.3 4.4-5.8 1.1-1 1.8 0 2-1 0-.8-3-2.1-3-2.1l-1.8 2.7"/>
|
||||||
|
<path fill="#452c25" d="M310.6 213.1s-3.4 6-2.5 6 4.5-7.5 4.5-7.5l-1.3.2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M310.6 213.1s-3.4 6-2.5 6 4.5-7.5 4.5-7.5l-1.3.2z"/>
|
||||||
|
<path fill="#452c25" d="M311.6 211.5s-3.6 5.8-2.7 5.9c1 .1 4.8-7.4 4.8-7.4l-1.3.1z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M311.6 211.5s-3.6 5.8-2.7 5.9c1 .1 4.8-7.4 4.8-7.4l-1.3.1z"/>
|
||||||
|
<path fill="#452c25" d="M312.3 210.5s-4 5.4-3.2 5.6c1 .2 5.4-7 5.4-7l-1.3.1z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M312.3 210.5s-4 5.4-3.2 5.6c1 .2 5.4-7 5.4-7l-1.3.1z"/>
|
||||||
|
<path fill="#452c25" d="M313.4 209.5s-4.8 4.9-3.9 5.2 6.2-6.2 6.2-6.2l-1.2-.1z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M313.4 209.5s-4.8 4.9-3.9 5.2 6.2-6.2 6.2-6.2l-1.2-.1z"/>
|
||||||
|
<path fill="#452c25" d="M313.6 207.7s-4.2 5.4-3.3 5.6 5.5-6.9 5.5-6.9l-1.3.1-1 1.2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M313.6 207.7s-4.2 5.4-3.3 5.6 5.5-6.9 5.5-6.9l-1.3.1-1 1.2z"/>
|
||||||
|
<path fill="#452c25" d="M312.5 212.4s-4 5.5-3.2 5.6c1 .2 5.4-7 5.4-7l-1.3.1z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M312.5 212.4s-4 5.5-3.2 5.6c1 .2 5.4-7 5.4-7l-1.3.1z"/>
|
||||||
|
<path fill="#452c25" d="M314.8 211.3s-2.4 4.5-2.1 4.6c.3.2 3.1-2.5 4.6-5.2 1.4-2.6-2.6.5-2.6.5"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M314.8 211.3s-2.4 4.5-2.1 4.6c.3.2 3.1-2.5 4.6-5.2 1.4-2.6-2.6.5-2.6.5"/>
|
||||||
|
<path fill="#452c25" d="M315 210.8s-2.3 5.5-1.5 6c0 0 3-3.3 3.7-5.9s0-.1 0-.1l-.2-2.9-2 2.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".2" d="M315 210.8s-2.3 5.5-1.5 6c0 0 3-3.3 3.7-5.9s0-.1 0-.1l-.2-2.9-2 2.7"/>
|
||||||
|
<path fill="#452c25" d="M313.8 210.4s-4.7 4.8-3.9 5.1 6.3-6.1 6.3-6.1l-1.3-.1-1 1z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M313.8 210.4s-4.7 4.8-3.9 5.1 6.3-6.1 6.3-6.1l-1.3-.1-1 1z"/>
|
||||||
|
<path fill="#452c25" d="M314.2 211s-4.7 5-3.9 5.2 6.2-6.2 6.2-6.2h-1.2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M314.2 211s-4.7 5-3.9 5.2 6.2-6.2 6.2-6.2h-1.2z"/>
|
||||||
|
<path fill="#452c25" d="M314.6 211.8s-4.8 4.9-4 5.2c1 .3 6.3-6.2 6.3-6.2h-1.2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M314.6 211.8s-4.8 4.9-4 5.2c1 .3 6.3-6.2 6.3-6.2h-1.2z"/>
|
||||||
|
<path fill="#452c25" d="M315 205.1s-3.6 4.5-3 5.3c.4.8 3.6-2 4.7-4 1-2-1.7-1.4-1.7-1.4"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M315 205.1s-3.6 4.5-3 5.3c.4.8 3.6-2 4.7-4 1-2-1.7-1.4-1.7-1.4"/>
|
||||||
|
<path fill="#452c25" d="M314.8 209.9s-3 5.8-2.2 5.5c.8-.4 3.8-4.7 4.2-5.7.3-1 .2-2 .2-2l-2.3 1.5.2 1.1"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M314.8 209.9s-3 5.8-2.2 5.5c.8-.4 3.8-4.7 4.2-5.7.3-1 .2-2 .2-2l-2.3 1.5.2 1.1"/>
|
||||||
|
<path fill="#452c25" d="M314.8 208s2.5-4.6 0 .9-3.4 4.5-3.4 4.5c-.2-.3 2.2-4 2.2-4s1.7-2.8 2.1-3.1"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M314.8 208s2.5-4.6 0 .9-3.4 4.5-3.4 4.5c-.2-.3 2.2-4 2.2-4s1.7-2.8 2.1-3.1"/>
|
||||||
|
<path fill="#452c25" d="M317.5 207.4s2.8-4.6 0 .9-3.9 4.5-3.9 4.5c-.2-.3 2.5-4 2.5-4s2-2.8 2.4-3.1"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M317.5 207.4s2.8-4.6 0 .9-3.9 4.5-3.9 4.5c-.2-.3 2.5-4 2.5-4s2-2.8 2.4-3.1"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M316.5 205.5s-3.5 4.5-3 5.2 3.7-2 4.7-4-1.7-1.4-1.7-1.4"/>
|
||||||
|
<path fill="#e8a30e" d="M352.8 251a32.8 37 0 1 1-65.6 0 32.8 37 0 1 1 65.6 0"/>
|
||||||
|
<path fill="none" stroke="#390" stroke-width=".8" d="M293.7 251c0-17 12-30.2 26.3-30.2s26.3 13.1 26.3 30.3" color="#000" font-family="Sans" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none"/>
|
||||||
|
<path fill="#007934" stroke="#eee" stroke-width=".1" d="M287.2 253c1 19.5 15.3 35 32.8 35s31.9-15.5 32.8-35z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".4" d="M352.8 251a32.8 37 0 1 1-65.6 0 32.8 37 0 1 1 65.6 0z"/>
|
||||||
|
<path fill="#d52b1e" stroke="#000" stroke-width=".1" d="M314.5 225.4q-.4 2 .7 3.6 1 1.4.9 2.7l-.7.5-5.5-3.7 3.7 5.5-.2.2a5 5 0 0 0-3-.3 5 5 0 0 1-3.7-.8 5 5 0 0 0 3.1 2.1 5 5 0 0 1 2.6 1.3l-.2.8-6.6 1.3 6.6 1.3v.3a5 5 0 0 0-2.4 1.9 5 5 0 0 1-3 2q2 .5 3.6-.7a5 5 0 0 1 2.7-.9l.4.7-3.6 5.6 5.4-3.7.3.2a5 5 0 0 0-.4 3 5 5 0 0 1-.7 3.6 5 5 0 0 0 2-3.1 5 5 0 0 1 1.4-2.5l.8.2 1.3 6.5 1.3-6.5h.3a5 5 0 0 0 1.8 2.3 5 5 0 0 1 2.1 3.1 4 4 0 0 0-.7-3.6 5 5 0 0 1-.9-2.8l.7-.4 5.5 3.7-3.7-5.5.2-.3q1.5.7 3 .4a5 5 0 0 1 3.7.7 5 5 0 0 0-3.1-2 5 5 0 0 1-2.6-1.4l.2-.8 6.6-1.3-6.6-1.2v-.4a5 5 0 0 0 2.4-1.8 5 5 0 0 1 3-2 5 5 0 0 0-3.6.7 5 5 0 0 1-2.7.8l-.4-.7 3.7-5.5-5.5 3.7-.3-.2q.7-1.5.4-3a5 5 0 0 1 .7-3.6 5 5 0 0 0-2 3 5 5 0 0 1-1.4 2.6l-.8-.2-1.3-6.5-1.2 6.5h-.4a5 5 0 0 0-1.8-2.3 5 5 0 0 1-2-3.1z" overflow="visible" style="marker:none"/>
|
||||||
|
<path d="M325.9 236.7c-1.7-1.4-3.8-1.6-4.9-.6q-1.3 1.7.2 3.8l-.3.2a5 5 0 0 1-.3-4.1c1.5-1.3 3.7-1.4 5.2.7"/>
|
||||||
|
<path d="M323.3 236.3c-1 0-1.2.2-1.6.5l-.7.4.1.2q.3 0 .9-.6.6-.4 1.3-.3c1.3 0 2 1 2.1 1s-.7-1.2-2.1-1.2"/>
|
||||||
|
<path d="M325 237.5c-1-1-2.7-1-3.4 0h.2c.8-1 2.5-.6 2.6 0"/>
|
||||||
|
<circle cx="323.1" cy="237.4" r=".6"/>
|
||||||
|
<path d="M321.6 237.6c.7.6 2.2.7 3.3-.1h-.5q-1.2 1-2.5 0"/>
|
||||||
|
<path d="M325 237.8c-1.2 1-2.4.9-3.1.5q-.9-.7-.6-.6l.8.4q.9.5 2.9-.2m-3.5 2a.4.4 0 1 1-.6.5c0 .1-.3.6-.9.6h-.1l.1.2.9-.2a.6.6 0 1 0 .6-1m.9 3c-.7-.5-1-1-1.7-1l-.7.1h-.1l.1.2c.3 0 .7-.3 1.2 0zm-.3 0c-1.4-.5-1.7-.2-2.1-.2h-.1l.1.3c.6 0 .9-.4 2.1-.1"/>
|
||||||
|
<path d="M322.4 243c-1.6-.2-1.1.8-2.4.8h-.1l.1.2c1.6 0 .9-.9 2.4-1m-8.2-6.3c1.6-1.4 3.7-1.6 4.8-.6q1.3 1.7-.2 3.8l.3.2a5 5 0 0 0 .3-4.1c-1.5-1.3-3.7-1.4-5.2.7"/>
|
||||||
|
<path d="M316.7 236.3c1 0 1.2.2 1.6.5l.7.4-.1.2-.9-.6q-.6-.4-1.3-.3c-1.3 0-2 1-2.1 1s.7-1.2 2.1-1.2"/>
|
||||||
|
<path d="M315 237.5c1-1 2.7-1 3.4 0h-.2c-.8-1-2.5-.6-2.6 0"/>
|
||||||
|
<circle cx="-316.9" cy="237.4" r=".6" transform="scale(-1 1)"/>
|
||||||
|
<path d="M318.4 237.6c-.7.6-2.2.7-3.3-.1h.5q1.3 1 2.5 0"/>
|
||||||
|
<path d="M315 237.8c1.2 1 2.4.9 3.1.5q1-.7.6-.6l-.8.4q-.8.4-2.9-.2m3.5 2a.4.4 0 1 0 .6.5c0 .1.3.6.9.6h.1l-.1.2-.9-.2a.6.6 0 1 1-.6-1m-.9 3c.7-.5 1-1 1.7-1l.7.1h.1l-.1.2c-.3 0-.7-.3-1.2 0zm.3 0c1.4-.5 1.7-.2 2.1-.2h.1l-.1.3c-.6 0-.9-.4-2.1-.1"/>
|
||||||
|
<path d="M317.6 243c1.6-.2 1.1.8 2.4.8h.1l-.1.2c-1.6 0-.9-.9-2.4-1"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-linecap="round" stroke-width=".1" d="M323.2 258.4q2.5.4 4.7-.3a10 10 0 0 1 4.2-.6c0-.2.5-.3.2-.5l-1.8-.6q-1.4-.8-2.7-2c-.1 0-.6-.3-.6-.5 2.2 3.2 7.5 1.5 12 1.2.4.1 1.5-.2 2.4-.5 1-.4 3.6 0 4.3-.5l-1.3-1c-.6-.8-2.3-.7-3-1.5-1.3-1.5-3.3-1.9-4.9-3q-.6-.3-1.6-.3c-.6-.6 0-.5-5-4.7-4.5-1.8-4.2-3.2-7-4.3-1-.5-1.9-1.4-2.7-1.1a30 30 0 0 0-6.3 3c-1.2.6-2.7 2-3.8 2.8-.2.2-3.4 2.9-7 4.8a115 115 0 0 1-9.2 4.8c8-.4-7.3 2.3 29.1 4.8z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M303.3 249c2-1 10-5.3 12.4-8.4-.4 0 .6 1 .5 1.4.8 0 .4-1 1-1q.7 0 1.3-.3c.5 0 .2.2.2.4q-1.2 1.7-3.2 2.9l.2.4q.7 0 1.1-.2l.1-.3q.5.3.1.7c-.3.6-1.4.5-1.8 1a6 6 0 0 1-1.5 1.7q.7-.8 1.7-1c1 0 1.3-.7 2.2-.9.9-.1 1.5-.9 2.2-1.4-.3.4-.9.7-.7 1.2l.6.2c-.7.8-2 1.4-2.4 2.4-.4-.2-.7.2-1 .2-.4.1-.3.8-.6 1q-1.7 1.2-2.3 2.7l-1.3.7c-.7.3-4.6 3.4-4.7 3-.3-2.1-4.3 1.6-13.3-1.6m30.6-.1-.2-.2c.1-.3-.5-.4-.5-.7 1 0 1.8 1.2 2.5.5.1-.1-.3-.4.4-.6l-.1-.2h-.8l-.8-.3q-.6-.3 0-.6c1-.1 1.9.5 2.6.2l1.7-.5c.3-.1 1.2 0 .9.2q-.4.2-1 .2-.8.2-1.4.6c.3 0 .2.3.7.2l2-.4v-.5h.3c-.3-.5.6-.2 1-.6l.1.1c-.5.2-.3.5-.4.8q-.2 0-.2.2c.2.2.2-.2.5 0h.6q.6 0 .5-.4-.5-.3-.6-.8l-.1-.1q.8 0 1.1.4t.8.7c.8.2.7-.2.7-.6.7 0 1.6.3 1.4.6q-.2.5-1 .5c-.8 0-.2.3-.4.3q-.7 0-1 .2-.2.4.3.9h1.8q.2-.4.9-.6c.4-.3-.2-.5-.5-.7-.3 0 0-.1 0-.3.3-.3.9 0 1-.2l.1-.8q.4 0 .4.3l.7-.2q.5.1.4.4-.8.3-.7.8c0 .2-.5.3-.3.5q.5.4.5.9.5.5 1.6.3c-.2-.7 1.4-.4 2-.5q.1 0 0-.2-.4-.2-.3-.6l-.1-.1c1 .3 2.8.8 3.6 1.4-1 .2-2.8-.2-3.8 0a15 15 0 0 1-3.7.6l-1.6-.3m-11.3-.7-.4-.1"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M321.2 259.1c2 0 2.2 1.4 3.5.1 1.1.2 2.2-.2 2.2-.3 2.7.6 11.1-.2 10.6-.8-.9-1-2.3-1.4-3.4-2.2l-1.1-.4c-.7-.2-1.5 0-2-.3l-2.6-1.5-1.2-1.4q-1-.5-2-.7-1.2-.7-2.2-1.9l-1.1-.4c-.7-.3-1.2-1.1-1.9-1-1 .2-1.7 1-2.8 1.6-1 .4-1.4 1.1-2.2 1.6-.2.2-2.6 1.8-5.3 2.9l-6.6 2.5s2.4 1.7 8 1.5l3.4 1 1.9-.3z"/>
|
||||||
|
<path fill="#007934" stroke="#000" stroke-width=".1" d="M309.8 254.8c1.2-.6 6-2.9 7.4-4.5-.1 0 .5.5.4.7.5 0 .2-.4.6-.5l.8-.1q.3 0 .1.2-.8.8-2 1.5l.2.2h.7v-.3q.4.2.1.4c-.2.4-.9.3-1.1.6l-1 .9q.5-.4 1.1-.5l1.3-.5q.9-.2 1.4-.8-.4.3-.4.7l.4.1c-.4.4-1.2.7-1.5 1.2l-.6.1-.4.6q-1 .6-1.4 1.4l-.8.4c-.4.2-2.8 1.8-2.8 1.6-.2-1.1-2.6.5-8-1.2"/>
|
||||||
|
<path fill="#00a6de" stroke="#000" stroke-width=".1" d="M320 214.2c-18.1 0-32.8 16.5-32.8 36.9S301.9 288 320 288s32.8-16.5 32.8-37c0-20.3-14.7-36.9-32.8-36.9zm0 8.2c13.2 0 24.7 12.3 24.7 28.7s-11.5 28.6-24.7 28.6-24.7-12.2-24.7-28.6 11.5-28.7 24.7-28.7z" color="#000" font-family="Sans" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none"/>
|
||||||
|
<path fill="#e8a30e" d="M324.7 266.8q.1.2-.4.3t-.5-.2q0-.2.5-.2.4-.2.4 0z"/>
|
||||||
|
<path fill="#e8a30e" d="m324.8 266.8-.5.2h-.5q0-.2.5-.3h.5zm-2.8 9c.8-2.2 1-3.8-.2-6 2-1.9 3.3-1.2 4.6 0-1.2 2.3-1 4-.2 6a4 4 0 0 1-4.2 0"/>
|
||||||
|
<path fill="#e8a30e" d="M324 268.6v7.7h.1v-7.7z"/>
|
||||||
|
<path fill="#e8a30e" d="M324.4 268.6a45 45 0 0 0-.3 7.7q0-4.2.4-7.6zm.7-2.4c-.4.4-.6.4-.4 1q.5-.4.4-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324.3 266.4c.2.3.6.4.3 1q-.4-.3-.3-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324.3 267c.2.3.5.3.3.9-.3-.3-.4-.3-.4-.8z"/>
|
||||||
|
<path fill="#e8a30e" d="M324.2 267.4q.4.2.3 1c-.4-.4-.3-.4-.3-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324 267.9q.6.1.4 1c-.4-.4-.2-.5-.4-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324 268.4c.2.2.6.3.4 1-.3-.4-.4-.4-.4-1m1.3-1.7q-.5 0-.6.8.6-.3.6-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M325.3 267.1q-.5.1-.7.7c.6-.2.5-.2.7-.7"/>
|
||||||
|
<path fill="#e8a30e" d="M325.2 267.6c-.2.1-.6 0-.7.7.6-.3.6-.2.7-.8z"/>
|
||||||
|
<path fill="#e8a30e" d="M325.2 268.1q-.5-.2-.7.7.4-.1.7-.7"/>
|
||||||
|
<path fill="#e8a30e" d="M325 268.6c-.4.1-.5 0-.6.7.5-.3.4-.2.5-.7zm.2.3c-.7 2.1-.8 4-.7 7.4h.1c0-3.4.1-5.2.8-7.3h-.1zm1.2-2.2c-.4.3-.6.2-.5.8q.5-.1.5-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M325.6 266.8q.5.2.1 1-.4-.4 0-1z"/>
|
||||||
|
<path fill="#e8a30e" d="M325.4 267.4c.3.3.5.3.2.8q-.3-.2-.2-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M325.3 267.7q.3.3.1 1c-.3-.4-.3-.4-.1-1"/>
|
||||||
|
<path fill="#e8a30e" d="M325 268.1q.6.4.3 1c-.4-.4-.2-.5-.3-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324.9 268.6q.5.4.2 1c-.3-.4-.3-.4-.2-1m1.6-1.4c-.2.2-.6 0-.7.7q.6-.1.7-.6z"/>
|
||||||
|
<path fill="#e8a30e" d="M326.4 267.7q-.4-.1-.8.5c.6-.1.6 0 .8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M326.3 268c-.3.2-.7 0-.9.7.7-.2.6-.2.9-.6z"/>
|
||||||
|
<path fill="#e8a30e" d="M326.1 268.6c-.2 0-.6-.2-.8.5.4 0 .4-.2.8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M325.8 269q-.4-.1-.7.6c.5-.2.5-.1.7-.6m.1.4c-1 1.9-1.1 3.6-.7 6.8l.2-.1c-.4-3-.2-4.7.6-6.6zm1.6-2.2c-.5.2-.7.2-.7.8q.5-.2.7-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M326.7 267.2c.1.3.4.5 0 1-.3-.5-.2-.5 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M326.4 267.8q.4.2 0 .8c0-.4-.2-.4 0-.9z"/>
|
||||||
|
<path fill="#e8a30e" d="M326.2 268c.1.3.4.5 0 1-.3-.4-.2-.4 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M326 268.5c0 .3.4.5 0 1-.3-.5 0-.5 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M325.7 268.9c.2.3.5.5 0 1q-.2-.4 0-1m1.8-1.1c-.2 0-.6-.1-.8.5.6-.1.5 0 .8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M327.3 268.2q-.4-.2-.8.4c.6 0 .6 0 .8-.4"/>
|
||||||
|
<path fill="#e8a30e" d="M327.1 268.5q-.4-.1-.8.5c.6 0 .5 0 .8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M327 269c-.3 0-.7-.3-1 .4.5 0 .5-.1 1-.4m-.5.4q-.4-.1-.7.5c.5-.1.5 0 .7-.5m-3-.7q.3 3.5.4 7.6h.1q.2-4.2-.4-7.6zm-.6-2.6c.4.4.5.4.4 1q-.5-.3-.4-1"/>
|
||||||
|
<path fill="#e8a30e" d="M323.7 266.4q-.6.2-.4 1 .5-.3.4-1"/>
|
||||||
|
<path fill="#e8a30e" d="M323.7 267q-.5.3-.3.8c.2-.3.3-.2.3-.7z"/>
|
||||||
|
<path fill="#e8a30e" d="M323.8 267.4q-.4.1-.3 1c.4-.4.3-.4.3-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324 267.9q-.6.1-.5 1c.5-.4.2-.5.4-1z"/>
|
||||||
|
<path fill="#e8a30e" d="M324 268.4q-.6.1-.4.9.4-.3.4-.9m-1.3-1.8c.2.2.6.2.6.8-.5-.3-.5-.2-.6-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M322.7 267.1q.5 0 .6.7c-.5-.3-.5-.2-.6-.7"/>
|
||||||
|
<path fill="#e8a30e" d="M322.8 267.5c.2.1.6.1.6.8q-.6-.2-.6-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M322.8 268c.2.1.7 0 .7.8q-.4-.2-.7-.7z"/>
|
||||||
|
<path fill="#e8a30e" d="M323 268.5c.3.2.5.1.5.8q-.5-.2-.5-.8m-.2.5-.2.1c.7 2 1 3.9.9 7.2h.1a19 19 0 0 0-.8-7.3m-1.2-2.4c.4.3.5.3.5.8q-.5-.1-.5-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M322.4 266.7q-.5.3-.2 1c.3-.4.3-.4.2-1"/>
|
||||||
|
<path fill="#e8a30e" d="M322.6 267.3q-.6.3-.2.9c.2-.4.3-.4.2-.9"/>
|
||||||
|
<path fill="#e8a30e" d="M322.7 267.6q-.5.4-.1 1c.3-.4.2-.4.1-1"/>
|
||||||
|
<path fill="#e8a30e" d="M323 268.1q-.6.3-.3 1c.4-.5.1-.5.2-1z"/>
|
||||||
|
<path fill="#e8a30e" d="M323 268.6c-.1.2-.5.4-.1 1q.3-.4.2-1zm-1.5-1.5c.2.1.6 0 .7.7-.6-.2-.5-.2-.7-.7"/>
|
||||||
|
<path fill="#e8a30e" d="M321.6 267.6q.4-.1.7.5c-.5-.1-.5 0-.7-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M321.7 268q.5-.1.8.6c-.6-.2-.6-.2-.8-.7z"/>
|
||||||
|
<path fill="#e8a30e" d="M321.9 268.5c.2 0 .6-.2.8.6q-.5 0-.8-.6"/>
|
||||||
|
<path fill="#e8a30e" d="M322.2 269q.4-.1.6.5c-.5-.2-.4-.1-.6-.6zm0 .5h-.2c.9 2 1 3.6.7 6.6h.2c.3-3 .2-4.6-.8-6.6zm-1.7-2.5c.4.3.6.2.6.8q-.5-.1-.6-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M321.3 267c-.1.3-.4.6 0 1 .3-.4.2-.4 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M321.6 267.6q-.5.3-.1.9c.1-.4.3-.4 0-.9z"/>
|
||||||
|
<path fill="#e8a30e" d="M321.8 268q-.4.2 0 1c.2-.5.2-.5 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M322 268.4c-.1.3-.4.5 0 1 .3-.5 0-.5 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M322.3 268.8q-.5.4-.1 1 .3-.4 0-1zm-1.8-1.2q.4-.1.8.5-.7.1-.8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M320.6 268q.5-.1.9.5c-.6-.1-.6 0-.9-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M320.8 268.4q.5-.1 1 .5c-.7-.1-.7 0-1-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M321 268.9c.3 0 .7-.3 1 .5-.5 0-.5-.3-1-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M321.4 269.3q.4-.1.7.5c-.5-.1-.4 0-.7-.5m2.2-3.2c.3.4.5.4.3 1q-.4-.4-.3-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324.3 266.4q-.4.1-.4 1 .6-.3.4-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324.4 267q-.6.2-.4.8c.2-.3.3-.2.4-.7z"/>
|
||||||
|
<path fill="#e8a30e" d="M324.4 267.4q-.4.1-.4 1c.4-.4.4-.4.4-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324.5 268q-.6 0-.5.8c.5-.3.3-.4.5-.9z"/>
|
||||||
|
<path fill="#e8a30e" d="M324.5 268.4c-.2.2-.6.3-.4.9.3-.3.4-.3.4-.9m-1.2-1.8c.2.2.6.2.6.8-.5-.3-.5-.3-.6-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M323.3 267q.4.1.6.8-.6-.2-.6-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M323.4 267.4c.2.2.6.2.6.9-.5-.4-.5-.3-.6-.9"/>
|
||||||
|
<path fill="#e8a30e" d="M323.4 268c.2.1.7 0 .6.8-.4-.2-.3-.4-.6-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M323.6 268.5c.2.1.4.1.4.8-.4-.4-.4-.3-.4-.8m-.4.3c.5 2.5.7 4.7.6 7.5h.2q.2-3.7-.7-7.5m-1-2.4c.4.4.6.3.5 1q-.5-.4-.5-1"/>
|
||||||
|
<path fill="#e8a30e" d="M323 266.6q-.5.2-.2 1c.3-.4.3-.4.2-1"/>
|
||||||
|
<path fill="#e8a30e" d="M323.1 267.2q-.5.2-.2.8.3-.3.2-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M323.3 267.5q-.5.3-.2 1 .4-.3.2-1"/>
|
||||||
|
<path fill="#e8a30e" d="M323.5 268q-.6.3-.3 1c.4-.4.2-.5.3-1"/>
|
||||||
|
<path fill="#e8a30e" d="M323.6 268.5c-.2.3-.6.4-.3 1q.4-.4.3-1M322 267q.6-.1.8.6c-.6-.2-.6-.1-.8-.6"/>
|
||||||
|
<path fill="#e8a30e" d="M322.1 267.4c.2.1.7 0 .8.6-.6-.2-.6-.1-.8-.6"/>
|
||||||
|
<path fill="#e8a30e" d="M322.3 267.8c.2.1.6 0 .7.7q-.7-.1-.7-.7"/>
|
||||||
|
<path fill="#e8a30e" d="M322.4 268.3c.2 0 .6-.1.8.7-.5-.1-.5-.3-.8-.7"/>
|
||||||
|
<path fill="#e8a30e" d="M322.7 268.8q.4-.1.6.6c-.5-.2-.5-.1-.6-.6m-.8.9q.9 2 .7 3.7l-.3 2.5h.1q1-3.6-.5-6.2"/>
|
||||||
|
<path fill="#e8a30e" d="m322.5 269.2-.1.1c.8 2.1.9 4 .7 7h.1c.2-3 .1-5-.7-7zm-1.4-2.4c.4.3.5.2.5.8q-.5-.1-.5-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M321.8 266.9c-.1.2-.4.4 0 1q.4-.4 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M322 267.4c-.1.3-.4.4 0 1 .1-.4.2-.4 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M322.3 267.8c-.2.2-.5.4 0 1 .2-.5.1-.5 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M322.5 268.2c-.2.3-.5.5 0 1 .2-.5 0-.5 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M322.7 268.7c-.1.2-.5.5-.1 1q.4-.4.1-1m-1.7-1.3q.4-.1.8.5c-.6-.1-.6 0-.8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M321.1 267.8q.5-.1.8.5c-.5-.1-.5 0-.8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M321.3 268.1c.2.1.7 0 .9.6-.6-.1-.6 0-.9-.6"/>
|
||||||
|
<path fill="#e8a30e" d="M321.5 268.7c.2 0 .6-.3.9.5-.5 0-.5-.2-.9-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M321.9 269c.3.1.4 0 .6.6-.4-.1-.4 0-.6-.5zm-2-1.6q.7.1.8.7-.5 0-.7-.7z"/>
|
||||||
|
<path fill="#e8a30e" d="M320.7 267.4c0 .3-.3.5.2 1 .2-.5.1-.5-.2-1"/>
|
||||||
|
<path fill="#e8a30e" d="M321 267.9c0 .3-.3.5.1.9.1-.4.2-.4 0-.9z"/>
|
||||||
|
<path fill="#e8a30e" d="M321.3 268.2c0 .3-.3.5.1 1 .2-.5.2-.5 0-1z"/>
|
||||||
|
<path fill="#e8a30e" d="M321.6 268.6c0 .3-.4.5.1 1 .2-.6 0-.5 0-1z"/>
|
||||||
|
<path fill="#e8a30e" d="M322 269c-.2.3-.5.6 0 1q.2-.4 0-1m-2-1q.4-.2.9.4c-.6 0-.6 0-1-.4z"/>
|
||||||
|
<path fill="#e8a30e" d="M320.2 268.4c.2 0 .6-.2.9.4-.6 0-.6 0-.9-.4"/>
|
||||||
|
<path fill="#e8a30e" d="M320.4 268.7q.5-.2 1 .4c-.6 0-.6 0-1-.4"/>
|
||||||
|
<path fill="#e8a30e" d="M320.7 269.2c.2 0 .6-.3 1 .4-.5 0-.5-.2-1-.4"/>
|
||||||
|
<path fill="#e8a30e" d="M321.1 269.5q.4-.1.8.5c-.5 0-.5 0-.8-.5m3.8-.7a26 26 0 0 0-.7 7.5h.2q-.2-4 .7-7.4zm1-2.3c-.4.3-.5.3-.5.9q.5-.2.5-.9"/>
|
||||||
|
<path fill="#e8a30e" d="M325.1 266.6q.5.4.2 1c-.3-.4-.3-.4-.2-1"/>
|
||||||
|
<path fill="#e8a30e" d="M325 267.3q.6.2.2.8c-.2-.3-.3-.3-.2-.9z"/>
|
||||||
|
<path fill="#e8a30e" d="M324.9 267.6q.5.2.2 1-.5-.4-.2-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324.7 268q.5.4.2 1c-.4-.4-.1-.5-.2-1"/>
|
||||||
|
<path fill="#e8a30e" d="M324.5 268.5c.2.3.6.5.3 1-.3-.4-.3-.4-.3-1m1.6-1.5c-.2.2-.6 0-.7.7q.6-.1.7-.7"/>
|
||||||
|
<path fill="#e8a30e" d="M326 267.5q-.4-.1-.7.6c.5-.2.5-.1.7-.6"/>
|
||||||
|
<path fill="#e8a30e" d="M325.9 267.9q-.5-.1-.8.6c.6-.2.6-.1.8-.6"/>
|
||||||
|
<path fill="#e8a30e" d="M325.7 268.4c-.2 0-.6-.2-.7.6q.4 0 .7-.6"/>
|
||||||
|
<path fill="#e8a30e" d="M325.4 268.8c-.2.1-.4 0-.5.7q.5-.1.5-.7m.2.4c-.9 2.1-1 4.1-.8 7h.2c-.2-3-.1-4.9.7-7zm1.5-2.3c-.5.3-.6.2-.6.8q.4-.1.6-.8"/>
|
||||||
|
<path fill="#e8a30e" d="M326.3 267c.1.2.4.5 0 1-.3-.5-.2-.5 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M326 267.5q.6.4.1 1c-.1-.5-.2-.5 0-1z"/>
|
||||||
|
<path fill="#e8a30e" d="M325.9 267.8c.1.3.4.5 0 1-.3-.4-.2-.4 0-1"/>
|
||||||
|
<path fill="#e8a30e" d="M325.6 268.3q.5.4.1 1c-.3-.5 0-.5-.1-1"/>
|
||||||
|
<path fill="#e8a30e" d="M325.4 268.7q.5.3.1 1c-.2-.4-.3-.4-.1-1m1.7-1.2c-.2.1-.6 0-.8.5.6 0 .6 0 .8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M327 268q-.4-.2-.8.4c.6-.1.5 0 .8-.5z"/>
|
||||||
|
<path fill="#e8a30e" d="M326.8 268.3q-.5-.1-.8.5c.6-.1.5 0 .8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M326.6 268.8c-.2 0-.6-.3-.8.5.4 0 .4-.3.8-.5"/>
|
||||||
|
<path fill="#e8a30e" d="M326.3 269.2q-.5-.1-.7.5.4 0 .6-.5zm0 .5c-1.1 2-1 4-.6 6.3h.2l-.3-2.9a6 6 0 0 1 .8-3.3h-.1zm1.9-2c-.5.1-.7 0-.8.6q.6 0 .8-.7z"/>
|
||||||
|
<path fill="#e8a30e" d="M327.4 267.5c0 .3.3.6-.2 1-.2-.5-.1-.5.2-1"/>
|
||||||
|
<path fill="#e8a30e" d="M327 268q.4.4 0 .9c-.1-.4-.2-.4 0-.9"/>
|
||||||
|
<path fill="#e8a30e" d="M326.8 268.3q.3.4-.1 1c-.2-.5-.2-.5.1-1"/>
|
||||||
|
<path fill="#e8a30e" d="M326.5 268.7c0 .3.4.6-.1 1-.2-.6 0-.5 0-1z"/>
|
||||||
|
<path fill="#e8a30e" d="M326.2 269c.1.4.4.7 0 1-.2-.4-.2-.4 0-1m2-.8c-.3 0-.6-.2-1 .4.6 0 .6 0 1-.4"/>
|
||||||
|
<path fill="#e8a30e" d="M327.9 268.6c-.2 0-.6-.2-.9.3.6 0 .6 0 .9-.3"/>
|
||||||
|
<path fill="#e8a30e" d="M327.7 268.9c-.3 0-.7-.2-1 .4.7 0 .6 0 1-.4"/>
|
||||||
|
<path fill="#e8a30e" d="M327.4 269.4c-.2 0-.6-.4-1 .3q.6 0 1-.3"/>
|
||||||
|
<path fill="#e8a30e" d="M327 269.7q-.4-.2-.8.4.5.1.8-.4m-3.5 2.7h1.1q1.1 0 1.2.4t-1.2.3h-1.1q-1.2 0-1.3-.4c-.1-.4.6-.3 1.3-.3"/>
|
||||||
|
<path fill="#e8a30e" d="m322.7 272.4-.4.6h.4l.4-.6zm.9 0-.4.6h.4l.4-.6zm.9 0-.4.7h.4l.4-.7zm.8 0-.3.7h.4l.3-.6h-.2z"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="m316.5 268.8.5 2.7.2.2v1.1l.5 1.6.3.6h.4l.1.2-.2.2h-.6l-.3-.1v-.3h-.2l-.1-.6-.8-1-.2-.5-.3-.3c-.5-1-1-3-1-3m-6.4-1 1.8.4-.5 2.6q-.4 1-.2 1 .2.7 1.1 2.3l.6.4.5.3h.5l.1-.2-.5-.2c0-.3-.5-.8-.5-1.2-.3-.7-.2-.7-.2-1.5l.5-1.6q.4-1 .5-2l-.8-1.7-.5-.7m-1.6-1.1c-2.9 1-1.7 3.5-.9 3.6m9.2-8.1.4-1v-.4l-.7.9"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M319.2 262.4h.4v-.6q-1-.4-1-.7v-.2c0-.2-.8-.3-1-.4l-.4-.2h-.2c-.8 0-1.1.7-1.2 1.2 0 0-.2 2.1-.5 3l-.2.3-.2.2-4.2-.2c-.8 0-1.8.7-1.8.7s-1 .6-1.1 1.6q-.1.5.1 1c1 2.7 1.8 0 2.1.1h.4c.5 0 1.4 1.5 2.7 1.8 4 .9 5-1.2 5-5.8v-.2l.2-.4v-.8l1-.2.2-.1"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="m317.3 261 .4-1v-.4l-.9 1m-8.9 7.6.7 1.8q-.1.6-.3.7h-.2l-.2.6v.6c0 .7.4 2.2.4 2.3q.2 0 .3.2v1.2q.1.3.3.3h.6l-.4-.4v-.2q0-.3.3-.5v-.3l-.1-.2-.1-.9v-1.1l.2-.2.1-.2 1-.6.8-1 .2-.5v-.2l-.3-.9-.6-.8m5.9 1.8c-.5.4-1.8.7-1.8 1 0 0 0 1.6-.2 2.2l-.3.6-.2.7-.3 1v.3l.4.3h-.4l-.6-.2V274l-.2-1.4.1-1-.1-3.6m4.7-5.4q.3.4 1.3.1.2.3.6-.2m-.3-.5h.2s.1 0 0 0z"/>
|
||||||
|
<path d="M317.8 261.4q.3.4.6.1-.3-.3-.5-.1z"/>
|
||||||
|
<path fill="#e7e7e7" fill-rule="evenodd" stroke="#000" stroke-width=".1" d="M329.1 272.9v2.9c.2.2 1 .2 1.2 0v-17.2h-.9v4.9l-.1.8v3.2q-.2.4 0 .8v.7l-.1.8z"/>
|
||||||
|
<path fill="#e7e7e7" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width=".1" d="M329.1 273.6c.3.2 1 .1 1.2 0m-1.2-.7h1.2m-1.2-.8h1.2m-1.1-.7h1.1m-1.1-.8h1.1m-1.1-.8h1.1m-1-.8h1m-1-.7h1m-1-.8h1m-1-.8h1m-1-.8h1m-1-.8h1m-1-.8h1m-1-.7h1m-1-.8h1m-1-.8h1m-.9-.8h.8m-.8-.8h.8m-.8-.8h.8m-.8-.7h.8m-1 15.5h1.1m-1.2.7h1.2"/>
|
||||||
|
<path fill="#007934" fill-rule="evenodd" stroke="#e7e7e7" stroke-width=".1" d="M331.5 257h2.1l-.8-.3h2.2l-1-.5 2.2.1-1-.5c.9 0 1.2 0 2 .2l-.9-.5 2.5-.2a9 9 0 0 0-8 .8l1-1.1h-.7q.5-.6 1.2-1l-.7.1 1.4-1-.9.1 1.6-1.1h-1l2-1.3c-3.3.4-5.1 2.8-5.3 4.8a7 7 0 0 0-6.6-3.6q1.5.5 2.4 1l-1 .1q1.1.3 2 1h-1l1.8.8-.8.1 1.5.8h-.8c1 .4 1.1.6 1.3.7a8 8 0 0 0-7.3 3l2.2-1-.5.8q.9-.6 2-1l-.4.7 1.8-1-.4.8 1.7-.7-.5.7 1-.4a6 6 0 0 0-2.9 5.8l.9-2v1l1-2v1l1-2v1l1.1-1.9v.8l.3-.6.7-.9.2.3q.4.7.5 1.8l.2-.9c.3.7.7 1.8.7 2.4l.2-1c.3.5.6 1.8.7 2.3l.3-.9q.4 1.3.5 2.3c.8-3-.4-4.9-1.8-6.3q.6.2 1.3 1l-.3-.9 1.5 1.4-.2-.8q.9.8 1.5 1.6l-.2-1a6 6 0 0 1 1.3 1.8l-.1-1q1 1 1.3 1.7c0-2.7-3.1-5-6-5.3z"/>
|
||||||
|
<path fill="none" stroke="#e7e7e7" stroke-linecap="round" stroke-linejoin="round" stroke-width=".1" d="M330.3 257.1c3.1-.5 7.2 2.3 7.2 5.3l-1.3-1.8.1 1-1.3-1.7.2 1q-.6-.8-1.5-1.6l.2.8-1.5-1.4.3 1q-.8-1-1.3-1.1m-1.8-1.5c-1.9-1.2-5.8-1-8.7 2.3l2.2-.9-.5.8q.9-.6 2-1l-.4.7 1.8-1-.4.8 1.7-.7-.5.7 1-.4"/>
|
||||||
|
<path fill="none" stroke="#e7e7e7" stroke-linecap="round" stroke-linejoin="round" stroke-width=".1" d="M329.7 256.6c-.4-2.2-3-4.8-6.9-4.6q1.5.5 2.4 1l-1 .1q1.1.3 2 1h-1l1.8.8-.8.1 1.5.7-.7.1c1 .4 1 .6 1.2.7m1.5 1c-2.6.8-5.1 3.3-4.7 6.7 0-.4.5-1.5.8-2v1l1-2v1l1-2v1l1.1-1.9v.8l1-1.5m-1.6.2-.5 1.4m-.5-.8-.5 1.9m-.5-1-.5 2"/>
|
||||||
|
<path fill="none" stroke="#e7e7e7" stroke-linecap="round" stroke-linejoin="round" stroke-width=".1" d="M329.9 258.6v-.6" class="bo-fil1 bo-str2"/>
|
||||||
|
<path fill="none" stroke="#e7e7e7" stroke-linecap="round" stroke-linejoin="round" stroke-width=".1" d="M332.6 261.3v1.3m-1-3 .1 1.6m-1-2.7.1 1.3m4.8-.4.6 1.2m-2-2.1.8 1.4m-2-1.8.7 1.2m-10.6-.8a5 5 0 0 1 1.7-1.2m-.2 1c.4-.6.6-.9 1.6-1.3m-.2 1q.5-.6 1.7-1.1m-.4 1.2q.7-.6 1.6-1m-3.7-4q1.2 0 2.2.6m-1.2.4q1 0 1.8.4m-1 .4q1 0 2 .6m-1.3.2a4 4 0 0 1 1.7.6m.9.2a9 9 0 0 1 8.5-1.2l-2.4.2q.6.1.8.4h-2q.6 0 1 .4H334l1 .3h-2.2l.8.3q-1.4 0-2 .2m4.8-1.6a6 6 0 0 0-2.2-.4m1 .7a6 6 0 0 0-2.5-.2m1.3.6a7 7 0 0 0-2.5-.2m1.3.7a5 5 0 0 0-2-.1m.8 1q.5.5.8 1.1m-3-3.1c.2-2 2-4.4 5.3-4.8l-2 1.3h1q-.9.4-1.6 1.1h.9l-1.4 1 .7-.1-1.2.9h.7l-1 1.1m2-4q-.8.2-1.6.7m1 .4-1.5.5m.9.4q-.7.2-1.5.7m1 .2a3 3 0 0 0-1.4.8m-.5 2.4-.3 1.1m1.2-2c1.8 1.6 4.1 3.7 3 7.6a6 6 0 0 0-.4-2.2l-.3.9a7 7 0 0 0-.7-2.4l-.2 1-.7-2.4-.2.9q0-1.4-.7-2"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-width=".1" d="m293.3 233.5 1.3.7.3-.6q.2-.5.1-.6 0-.3-.5-.5t-.7-.2q-.3 0-.5.4l-.2.5q0 .2.2.3m1.6 1 1.7.8h.3l.3-.5.3-.6v-.4l-.2-.2-.3-.2q-1.1-.6-1.7.3l-.4.7m-2.6-.8.6-1.2.6-.7.6-.4h.9q.3.1.4.6.2.3.1.7l1-.6 1 .1q.4.3.6 1t-.4 1.7l-.7 1.3-.3.7-1.4-.9-1.8-1-1.5-.7.3-.6m9-10.8q-1.2 1 .3 2.6.8 1 1.5 1 .6.2 1.3-.3.5-.4.4-1a3 3 0 0 0-.8-1.5q-.8-1-1.4-1.2-.8-.1-1.4.4m3.8-.2q.7 1 .7 2a3 3 0 0 1-1.2 2 3 3 0 0 1-2 .7 2 2 0 0 1-1.8-1 3 3 0 0 1-.7-2q0-1 1-2a3 3 0 0 1 2-.7q1.1 0 2 1m8-2.6.3 1 1.9-.7h.3l.3.7-.5.1-2.4.7-.7.2-.4-1.6-.5-2-.5-1.5.6-.2.7-.2.3 1.6.6 2m9.2-3v-1.6h.8l.7.1-.3 1.7-.2 2-.1 1.6h-.7l-.7-.1.3-1.7.2-2m9.9 5.3 3.1-2.7.4.2.4.2-4.7 3.6-.5-.3.2-2 .4-3.8 1.2.7-.5 4m8.4 4 1.2-1.3.4.6.5.4-1.3 1.1-1.5 1.4-1.1 1.1-.5-.5-.5-.5 1.3-1 1.5-1.4m5 10 1.3-1.4h-2l.7 1.4m-1.1-1.3-2 .1-.1-.3-.2-.4h5.9l.3.6-4 4.3-.2-.7-.3-.6 1.4-1.3-.4-.8-.4-1" font-family="Linux Biolinum" font-size="100" font-weight="700" letter-spacing="60" style="line-height:125%;text-align:center" text-anchor="middle" word-spacing="0"/>
|
||||||
|
<path fill="#e8a30e" stroke="#000" stroke-linecap="square" stroke-linejoin="round" stroke-width=".1" d="m325 280.6-.8 2.2h-2.4l1.9 1.5-.7 2.2 2-1.3 1.8 1.3-.6-2.2 1.8-1.4h-2.3zm9-3.9-.7 2.2H331l1.8 1.5-.7 2.2 2-1.3 1.9 1.3-.7-2.2 1.9-1.4h-2.4zm14.2-25-.8 2.3h-2.3l1.8 1.4-.6 2.3 1.9-1.4 1.9 1.4-.7-2.3 1.9-1.4h-2.4zm-6.7 17.9.7 2.2h2.4l-1.9 1.4.7 2.3-2-1.4-1.9 1.4.7-2.3-1.8-1.4h2.3zm4.7-8.2.8 2.2h2.3l-1.9 1.5.7 2.2-1.9-1.3-2 1.3.8-2.2-1.9-1.4h2.3zm-31.1 19.2.8 2.2h2.3l-1.9 1.5.7 2.2-2-1.3-1.8 1.3.6-2.2-1.8-1.4h2.3zm-9.2-3.9.8 2.2h2.4l-2 1.5.8 2.2-2-1.3-1.9 1.3.7-2.2-1.9-1.4h2.4zm-14-25 .7 2.3h2.3l-1.8 1.4.6 2.3-1.9-1.4-1.9 1.4.7-2.3-1.9-1.4h2.4zm6.6 17.9-.7 2.2h-2.4l1.9 1.4-.7 2.3 2-1.4 1.9 1.4-.7-2.3 1.8-1.4h-2.3zm-4.7-8.2-.8 2.2h-2.3l1.9 1.5-.7 2.2 1.9-1.3 2 1.3-.8-2.2 1.9-1.4h-2.3z"/>
|
||||||
|
<path fill="#e7e7e7" d="M321 248.1v-.5h.1l-.8-.5h-.7l-.8.5v.5h2.3"/>
|
||||||
|
<path fill="#e7e7e7" d="M321 248.1v-.5h.1l-.8-.5v-.7h-.6v.7l-.9.5v.5h2.3zm.3.6v.2h-2.6v-.2z"/>
|
||||||
|
<path fill="#e7e7e7" d="M321.3 248.7v.2h-2.6v-.2zm-2.5 0v-.5h.1v.5-.5h-.1v-.1h2.4v.1h-.2v.5-.5h.1v.5"/>
|
||||||
|
<path fill="#e7e7e7" d="M318.9 248.7v-.5zv-.5h-.1v-.1h2.4v.1h-.2v.5-.5h.1v.5"/>
|
||||||
|
<path fill="#e7e7e7" d="M319.4 248.6v-.4h-.4v.4z"/>
|
||||||
|
<path fill="#e7e7e7" d="M319.1 248.3v.2h.2v-.2zm1.8.3v-.4h-.3v.4z"/>
|
||||||
|
<path fill="#e7e7e7" d="M320.7 248.3v.2h.1v-.2zm.2-.3v-.3h-.3v.3z"/>
|
||||||
|
<path fill="#e7e7e7" d="M320.7 247.7v.2h.1v-.2zm-1.3.3v-.3h-.4v.3z"/>
|
||||||
|
<path fill="#e7e7e7" d="M319.1 247.7v.2h.2v-.2zm.8.3v-.3h-.3v.3z"/>
|
||||||
|
<path fill="#e7e7e7" d="M319.7 247.7v.2h.1v-.2zm.7.3v-.3h-.3v.3z"/>
|
||||||
|
<path fill="#e7e7e7" d="M320.1 247.7v.2h.2v-.2zm.4.2v.8h-1v-.8z"/>
|
||||||
|
<path fill="#e7e7e7" d="M320.5 247.9v.8h-1v-.8zm-1 .2h1m-.9.6v-.6m.7.6v-.6m.5-.5-.5-.4h-.6l-.5.4zm-1-.5h.4m-.3-.1h.2zm.1 1h.1zm.3 0h.1"/>
|
||||||
|
<path fill="#e7e7e7" fill-rule="evenodd" d="M319.8 246h.4v.4h-.4z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M320 245.6v.4m-.2-.2h.4"/>
|
||||||
|
<path fill="#452c25" d="M317.1 210.3s-2.3 5.4-1.5 6c0 0 3-3.3 3.7-5.9s0-.2 0-.2l-.2-2.8-2 2.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".2" d="M317.1 210.3s-2.3 5.4-1.5 6c0 0 3-3.3 3.7-5.9s0-.2 0-.2l-.2-2.8-2 2.7"/>
|
||||||
|
<path fill="#452c25" d="M317.6 207.7s-2.8 6-2.1 6.4c0 0 2.3-2.7 3-4.8.5-2.1 0-.1 0-.1l.7-3.9"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M317.6 207.7s-2.8 6-2.1 6.4c0 0 2.3-2.7 3-4.8.5-2.1 0-.1 0-.1l.7-3.9"/>
|
||||||
|
<path fill="#452c25" d="M320.5 206.4s-2.8 6-2.1 6.4c0 0 2.3-2.7 3-4.8.5-2.2 0-.2 0-.2l.7-3.8"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M320.5 206.4s-2.8 6-2.1 6.4c0 0 2.3-2.7 3-4.8.5-2.2 0-.2 0-.2l.7-3.8"/>
|
||||||
|
<path fill="#452c25" d="m356.8 195.6 3.4 1.9s.8.7-1 .3a42 42 0 0 1-12.8-6.4c-3.3-2-4.3-2-4.3-2l4.5.1z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m356.8 195.6 3.4 1.9s.8.7-1 .3a42 42 0 0 1-12.8-6.4c-3.3-2-4.3-2-4.3-2l4.5.1z"/>
|
||||||
|
<path fill="#452c25" d="m358 194.8 3.3 1.9s.8.7-1 .3a45 45 0 0 1-12.8-6.4c-3.3-2.1-.6 1.7-.6 1.7l.5-2.5 10.5 5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m358 194.8 3.3 1.9s.8.7-1 .3a45 45 0 0 1-12.8-6.4c-3.3-2.1-.6 1.7-.6 1.7l.5-2.5 10.5 5z"/>
|
||||||
|
<path fill="#452c25" d="M363.5 196.6s-4-.7-5.4-1.8c0 0 .2.5-1.7-.4 0 0 .7 1.7-4.8-2s-3.6-2-3.6-2l1.6-.3 9.3 3.7z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M363.5 196.6s-4-.7-5.4-1.8c0 0 .2.5-1.7-.4 0 0 .7 1.7-4.8-2s-3.6-2-3.6-2l1.6-.3 9.3 3.7z"/>
|
||||||
|
<path fill="#452c25" d="m342.6 198.1 1.4 1.9s-.5 1.8-5.1-1.7-4.4-3.2-4.4-3.2l2.6-.3 5.6 3.2"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m342.6 198.1 1.4 1.9s-.5 1.8-5.1-1.7-4.4-3.2-4.4-3.2l2.6-.3 5.6 3.2"/>
|
||||||
|
<path fill="#452c25" d="M336.6 199s2.2 2.9 1.7 3.2-3 .2-4.8-2.5c-1.9-2.8 0-.2 0-.2v-4.4z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M336.6 199s2.2 2.9 1.7 3.2-3 .2-4.8-2.5c-1.9-2.8 0-.2 0-.2v-4.4l3.2 3.8"/>
|
||||||
|
<path fill="#452c25" d="M338.8 197.8s2.1 2.6 1.9 3-3.2.2-5.5-2.5c-2.3-2.6-.3-3.4-.3-3.4l3.9 2.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M338.8 197.7s2.1 2.7 1.9 3.1-3.2.2-5.5-2.5c-2.3-2.6-.3-3.4-.3-3.4z"/>
|
||||||
|
<path fill="#452c25" d="M350.6 196.2s6 2.5 1.6 2.4c0 0-8.6-2.3-13-6.2l1.2-1.6 10.2 5.2"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M350.6 196.2s6 2.5 1.6 2.4c0 0-8.6-2.3-13-6.2l1.2-1.6 10.2 5.2"/>
|
||||||
|
<path fill="#452c25" d="M353.8 195.4s3.1 2 3.4 2.7-10-1.9-15.3-6.3l2.4-1.2 9.4 4.8z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M353.8 195.4s3.1 2 3.4 2.7-10-1.9-15.3-6.3l2.4-1.2 9.4 4.8z"/>
|
||||||
|
<path fill="#452c25" d="M344.5 197.2s2.3 1.9 2 2.1-5-.5-8.6-3.3l.4-1.7z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M344.5 197.2s2.3 1.9 2 2.1-5-.5-8.6-3.3l.4-1.7 6.2 2.8"/>
|
||||||
|
<path fill="#452c25" d="M348.4 197s2.3 1.6 1.8 1.8-2.2 1.6-10.7-3.4l-1-.6 1.3-2z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M348.4 197s2.3 1.6 1.8 1.8-2.2 1.6-10.7-3.4l-1-.6 1.3-2z"/>
|
||||||
|
<path fill="#452c25" d="M339.7 192.4s2.8 2.4 2.4 2.8-3.6-.4-5-1.5c-1.5-1-2.6-2.4-2.6-2.4l3-.7z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M339.7 192.4s2.8 2.4 2.4 2.8-3.6-.4-5-1.5c-1.5-1-2.6-2.4-2.6-2.4l3-.7z"/>
|
||||||
|
<path fill="#452c25" d="m336.4 188.3 5 3s4.1 2.8 3.7 3.1-3.7-.8-6-2c-2.2-1.3-5-4-5-4"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m336.4 188.3 5 3s4.1 2.8 3.7 3.1-3.7-.8-6-2c-2.2-1.3-5-4-5-4"/>
|
||||||
|
<path fill="#452c25" d="M333.2 202.4s1 2.4.4 2.6-2-.2-3-2.3c-1.2-2.2 1-1.3 1-1.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M333.2 202.4s1 2.4.4 2.6-2-.2-3-2.3c-1.2-2.2.7-1.3.7-1.3z"/>
|
||||||
|
<path fill="#452c25" d="M334.9 200.9s1.4 2.3 1.1 2.6c-.3.2-2.2 1-4.2-2s2.1-2.3 2.1-2.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M334.9 200.9s1.4 2.3 1.1 2.6c-.3.2-2.2 1-4.2-2s2.1-2.3 2.1-2.3z"/>
|
||||||
|
<path fill="#452c25" d="M330.7 190.4s4.8 9.3 4.4 9.8-2.3 0-3.4-2.5-1.9-5.6-1.9-5.6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M330.7 190.4s4.8 9.3 4.4 9.8-2.3 0-3.4-2.5-1.9-5.6-1.9-5.6z"/>
|
||||||
|
<path fill="#452c25" d="M336.3 192.8s4 3.3 3.2 3.8q-1.2.8-4.8-2.5c-2.3-2.3 1.6-1.5 1.6-1.5"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M336.3 192.7s4 3.5 3.2 3.9q-1.4.8-4.8-2.5c-3.4-3.3 1.6-1.4 1.6-1.4z"/>
|
||||||
|
<path fill="#452c25" d="M334.4 192.8s2.8 5.3 2.5 5.8c-.4.5-2.5-1.3-3.6-2.7-1-1.3-1.9-3.3-1.9-3.3"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M334.4 192.8s2.8 5.3 2.5 5.8c-.4.5-2.5-1.3-3.6-2.7-1-1.3-1.9-3.3-1.9-3.3"/>
|
||||||
|
<path fill="#452c25" d="M312.9 203.6s-.2 3.1 0 3.3c.2.3 1.7.3 1.8-2 .1-2.4-.3-2.5-.3-2.5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M312.9 203.6s-.2 3.1 0 3.3c.2.3 1.7.3 1.8-2 .1-2.4-.3-2.5-.3-2.5l-1.5 1.1"/>
|
||||||
|
<path fill="#452c25" d="M313.8 199.9s-1 3.4 0 4.1 1.9-3.4 2-4.3c0-1-2 .2-2 .2"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M313.8 199.9s-1 3.4 0 4.1 1.9-3.4 2-4.3c0-1-2 .2-2 .2z"/>
|
||||||
|
<path fill="#452c25" d="M314.6 204.3s.2 3.2.8 3.4 1.6-1 1.6-1.8-1-2.8-1-2.8l-1.4 1"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M314.6 204.3s.2 3.2.8 3.4 1.6-1 1.6-1.8-1-2.8-1-2.8l-1.4 1"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M314.6 200.4s-1 3.5 0 4.2 1.9-3.4 2-4.4c0-.9-2 .2-2 .2z"/>
|
||||||
|
<path fill="#452c25" d="M314.7 194.8s-1.6 1.7-1.6 2.6 2.4-1.2 2.6-1.6-1-1-1-1"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M314.7 194.8s-1.6 1.7-1.6 2.6 2.4-1.2 2.6-1.6-1-1-1-1z"/>
|
||||||
|
<path fill="#452c25" d="M313.6 194s-1.3 2.3-1 3 1.6-.5 2.2-1.4-1.2-1.7-1.2-1.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M313.6 194s-1.3 2.3-1 3 1.6-.5 2.2-1.4-1.2-1.7-1.2-1.7z"/>
|
||||||
|
<path fill="#452c25" d="M331.5 190.8s2 3.8 1.5 4q-1 .1-2.3-1.9c-.8-1.1.8-2.1.8-2.1"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M331.5 190.8s2 3.8 1.5 4q-1 .1-2.3-1.9c-.8-1.1.8-2.1.8-2.1z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M331.6 198.6s.7 3.3 0 3.6c-1.2.5-2-2.3-2-3.4s2-.2 2-.2zm-2.6 5.1s.2 2.5-.2 2.7c-.3.2-1.2.2-2.1-1.8l-.5-1.2 2.3-1.2.5 1.3"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M331.4 203.2s.2 2.5-.3 2.6c-.5 0-1.9-.8-2.5-2.3-.6-1.8 2.4-1.3 2.4-1.3z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M329 195.3s2.6 6 2.1 6.7c-.8 1.7-3.2-3.5-3.9-5.6-.8-2.2 1.7-1 1.7-1z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M327.7 196.3s3.6 6.5 2.1 6.6c-1.5.2-4.3-4.6-4.7-5.8-.5-1 2.6-.8 2.6-.8z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M327.5 197.7s1.8 6.2.6 5.6c-1-.5-2.5-5.2-2.6-6.2s2 .6 2 .6zm6.2-8.8s2.7 4.1 1.8 4.2c-.8.2-4-2.7-4-3.2-.1-.5 2.2-1 2.2-1z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M332 189s3.5 4.8 2.4 5c-1 .2-1.1-.5-1.1-.5s-2.8-2.5-3-3.1c-.1-.6 1.6-1.4 1.6-1.4m3.5.1s2.4 2.5 1.8 3.3-4-3.1-4.4-3.6 2.9.2 2.9.2m-6 3.5s3.8 7.5 3 8.2c-.6.8-4.9-5.6-5-6.5-.2-.9 2-1.8 2-1.8"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M327.5 193.2s1.5 2.3 1.4 3.6c0 1.2-2.3-1.8-2.5-2.4s1-1.2 1-1.2zm3.1-1.2s1.2 2.4.8 3.1-2-1.4-2.5-2.3 1.7-.8 1.7-.8z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".2" d="M325 194.4s2 2 1.9 3.1-2.8-1.6-3-2c-.3-.6 1.2-1.1 1.2-1.1z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M337.2 188.4s2.8 2.5 2.4 3c-.5.3-4.3-2.4-4.9-3-.6-.5 2.5 0 2.5 0z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="m334.8 188.2 2.2 1.9s2 1.3 1.7 1.7-3.6-1.1-4.1-1.8q-.8-1.2-.6-1.4zm7.3.6s8.4 3.4 8 4.2c-.3.8-8.8-2.7-10-3.7-1.4-.9 1.8-.6 1.8-.6"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M342.4 189.8s6.3 3.6 5.5 4-5.2-1-7.5-2.5l-3.3-2.5 2.8-.6zm-15.5 4.4s1.2 2.4.8 3c-.4.8-1.5-.5-2.1-1.4s1.3-1.6 1.3-1.6zm.3 5.4s1 3.5-.1 4.2c-1.1.6-1.8-3.6-1.8-4.5s1.9.3 1.9.3z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M322.3 193.8s3.5-1.5 4.1-1.4c0 0 .8-.2 1.1-.5l1.3-1s-.6-4.3 3.9-3.8l11.5 1.1a45 45 0 0 1 6.5 2l9 3.9 4 2.3c.8.5-.1.1-.1.1s-10.7-5.7-14.5-6.4c-1-.2 0 1 0 1l-3.4-1.3a6 6 0 0 0-2.9-.6 6 6 0 0 1-2.2-.3c-.6-.2-3.8-.3-4.4-.4l-1-.2.3.4-1.6-.2-.5.6s-1.5.3-1.6-.2-1 2.3-1.3 3c-.4.9-1.9.9-2.3 1.4l-1 .9c-.2.1-1.3.8-1.7.8l-2.9.2-.6-1zm4.6 10-.1 3.3c-.2.2-1.8.1-1.8-2.2s.5-2.4.5-2.4l1.5 1.2"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M327.3 201.4s-.3-1.4-.5-1.6 0-.3 0-.3-.5-1.4-.9-1.6c-.3-.2.1-.4.1-.4s-.6-1-1-1.2c-.4-.3 0-.4 0-.4s-.5-1.2-1.5-1.8c0 0-.7-.7-1.4-.9q-1.2-.4-4.5-.3c-2.2 0-3.2 1.5-3.2 1.5l-.1 1.8.3-.2-.5 2.2c0 .5.5 1.4.5 2.5v1q.2 1.5.8 2.9v.2c.2-.2.5.6.7 1 0 0 0 1 .2.6l.6 1.1c0 .2.5 1.4.5.9 0-.6.4 1.3.4 1.5l.5-.7.2.8h.5l-.2.8s1.2-1 1.3-1.4v-.6l.4-.4.6-1s1.5 1.2 1.7 1.7l.3.7.4-.3.3.8.3-.4.2.6.1.3c.1.1.4.2.8-.6q.8-1.7.6-2c0-.3.3.2.3.2s.6-1 .5-1.6.3-.4.3-.4v-2.2q.2-.4.3-.3l-.2-2.3c-.2-.2.3-.2.3-.2z"/>
|
||||||
|
<path d="M324.3 196.7q.4.1.6.6-.2-.6-.6-.8zm.3 1.2q.7.8.5 1.8v.1q.1-1.1-.5-2zm1.1.7a3 3 0 0 1 .4 1.6q.1-1-.4-1.7m.4 2.5q0 .6-.2 1v.2zm-.4 1.9v.7-.8m.7 0q.3.2.3.6v-.1l-.3-.7zm0-2.6.4.5v-.2l-.5-.5zm-1.4 2v1zm-.7.7v1-1.1z"/>
|
||||||
|
<path fill="#452c25" d="M310.7 194.3s-.8 3.2-.2 3.5c1.3.7 2-2.2 2.2-3.2.1-1.2-2-.3-2-.3"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M310.7 194.3s-.8 3.2-.2 3.5c1.3.7 2-2.2 2.2-3.2.1-1.2-2-.3-2-.3z"/>
|
||||||
|
<path fill="#452c25" d="M312.6 194.3s-1.6 1.7-1.6 2.6 2.4-1.3 2.6-1.7-1-.9-1-.9"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M312.6 194.3s-1.6 1.7-1.6 2.6 2.4-1.3 2.6-1.7-1-.9-1-.9z"/>
|
||||||
|
<path fill="#452c25" d="M285 194.5s-3.3 1.7-3.6 2.4 10-1 15.8-5l-2.3-1.4z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M285 194.5s-3.3 1.7-3.6 2.4 10-1 15.8-5l-2.3-1.4z"/>
|
||||||
|
<path fill="#452c25" d="M289 195.2s-6 2.2-1.7 2.3c0 0 8.7-1.8 13.3-5.4l-1.1-1.7z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M289 195.1s-6 2.3-1.7 2.4c0 0 8.7-1.8 13.3-5.4l-1.1-1.7z"/>
|
||||||
|
<path fill="#452c25" d="M298 189s-6.5 3.4-5.8 3.7 5.3-.6 7.7-2l3.4-2.4-2.9-.8-2.5 1.6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M298 189s-6.5 3.4-5.8 3.7 5.3-.6 7.7-2l3.4-2.4-2.9-.8-2.5 1.6z"/>
|
||||||
|
<path fill="#452c25" d="M295.5 196.3s-2.3 1.9-2 2.1 5-.5 8.6-3.4l-.5-1.7z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M295.5 196.3s-2.3 1.9-2 2.1 5-.5 8.6-3.4l-.5-1.7-6.2 2.9"/>
|
||||||
|
<path fill="#452c25" d="M291.5 196s-2.2 1.7-1.7 1.8 2.3 1.5 10.6-3.9l1-.6-1.5-2-8.4 4.5"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M291.5 196s-2.2 1.7-1.7 1.8 2.3 1.5 10.6-3.9l1-.6-1.5-2z"/>
|
||||||
|
<path fill="#452c25" d="M289.8 189.6a66 66 0 0 0-13.9 7.2l17.9-7m11.2 10.6s-1.5 2.3-1.2 2.5 2.1 1.2 4.3-1.7c2-3-2-2.5-2-2.5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M305 200.4s-1.5 2.3-1.2 2.5 2.1 1.2 4.3-1.7c2-3-2-2.5-2-2.5z"/>
|
||||||
|
<path fill="#452c25" d="M306.6 202s-1 2.4-.5 2.6 2-.1 3.2-2.2c1.2-2.2-.6-1.1-.6-1.1zm-3.3-3.5s-2.2 2.7-1.7 3.1c.4.4 3 .3 4.9-2.4l-.5-2.5-2.7 1.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M306.6 202s-1 2.4-.5 2.6 2-.1 3.2-2.2c1.2-2.2-.6-1.1-.6-1.1zm-3.3-3.5s-2.2 2.7-1.7 3.1c.4.4 3 .3 4.9-2.4l-.5-2.5-2.7 1.7"/>
|
||||||
|
<path fill="#452c25" d="M301.2 197.1s-2.2 2.6-2 3c.2.5 3.2.3 5.6-2.2s.5-3.5.5-3.5l-4 2.6"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M301.2 197.1s-2.2 2.6-2 3c.2.5 3.2.3 5.6-2.2s.5-3.5.5-3.5l-4 2.6"/>
|
||||||
|
<path fill="#452c25" d="m297.4 197.3-1.4 1.9s.3 1.8 5.1-1.5l4.5-3-2.5-.5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m297.3 197.2-1.3 2s.3 1.8 5.1-1.5l4.5-3-2.6-.5z"/>
|
||||||
|
<path fill="#452c25" d="m282.2 194.5-3 1.3s-.7.6 1 .7c1.8 0 5.5-.5 11.9-4 2.8-1.7 4.7-3.3 4.7-3.3l-2.4-.8-12.2 6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m282.2 194.5-3 1.3s-.7.6 1 .7c1.8 0 5.5-.5 11.9-4 2.8-1.7 4.7-3.3 4.7-3.3l-2.4-.8-12.2 6z"/>
|
||||||
|
<path fill="#452c25" d="M277 196.4s4.4-.5 6-1.6c0 0-.3.5 2-.4 0 0-1.2 2 5.3-2 6.5-3.8 0 0 0 0l7.2-4.1-.4-.8-14.7 6.2c-1 .3-5.4 2.7-5.4 2.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m290.3 192.4 7.2-4.1-.4-.8-14.7 6.2c-1 .3-5.4 2.7-5.4 2.7s4.4-.5 6-1.6c0 0-.3.5 2-.4 0 0-1.1 2 5.3-2z"/>
|
||||||
|
<path fill="#452c25" d="M308.5 202s-.4 3.4.1 3.5 2-1 2.6-3c.6-2.2-2.4-2-2.4-2l-.4 1.3"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".2" d="M308.5 202s-.4 3.4.1 3.5 2-1 2.6-3c.6-2.2-2.4-2-2.4-2l-.4 1.3"/>
|
||||||
|
<path fill="#452c25" d="M310.8 203.5s-.3 2.4 0 2.6c.4.3 1.3.4 2.3-1.6s.5-1.3.5-1.3l-2.3-1.2-.5 1.3"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M310.8 203.4s-.3 2.5 0 2.7c.4.3 1.3.4 2.3-1.6s.5-1.3.5-1.3l-2.3-1.2z"/>
|
||||||
|
<path fill="#452c25" d="M311.4 199s-1.9 4-.8 4 2.5-3 2.7-3.7-1.9-.3-1.9-.3"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M311.4 199s-1.9 4-.8 4 2.5-3 2.7-3.7-1.9-.3-1.9-.3z"/>
|
||||||
|
<path fill="#452c25" d="M311.3 199s-.8 4.5.2 4c1-.3 1.6-3.7 1.7-4.4s-2 .4-2 .4"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M311.3 199s-.8 4.5.2 4c1-.3 1.6-3.7 1.7-4.4s-2 .4-2 .4z"/>
|
||||||
|
<path fill="#452c25" d="M312.8 199.4s-1.1 3.5 0 4.2c1 .7 1.8-3.4 1.9-4.3 0-1-2 .1-2 .1"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M312.8 199.4s-1.1 3.5 0 4.2c1 .7 1.8-3.4 1.9-4.3 0-1-2 .1-2 .1z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M308.4 198.3s-1 3.2-.4 3.5c1.2.6 2.3-2.2 2.4-3.3.1-1.2-2-.2-2-.2zm-7.9-6.6s-2.8 2.4-2.5 2.7 3.6-.2 5.1-1.3l2.1-1.5-2.5-1.6z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M303 189.3s-1.8 1.5-1.5 1.9 3.6-1 4.2-1.7.6-1.3.6-1.3z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M304.9 189s-2.5 2.1-1.9 2.9 4-2.6 4.5-3-2.9 0-2.9 0"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="m303.8 188.2-5 2.3s-4.2 2.7-3.8 3 3.7-.6 6-1.7a38 38 0 0 0 5-3.1m-.9 2.1s-5.3 4.7-4.5 5.1c.8.5 4.4-1.4 6.8-3.7"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M305.8 192.4s-3 5-2.7 5.6c.4.5 2.6-1.2 3.7-2.5s2-3.2 2-3.2"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M307.6 192.8s-3.3 6.1-3 6.6c.4.5 2.4.1 3.5-2.3s1-4 1-4z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M309.5 193.1s-3.7 7.6-3 8.3 4.8-5.7 5-6.6c.3-1.6-2-1.8-2-1.8m-7.7-4.9s-3.3 1.6-2.7 2c.7.2 3.8-1.2 4.2-1.3s-1.5-.7-1.5-.7z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M302.3 188.6s-2 1.6-1.6 2c.4.5 3.5-1.2 4-1.7.7-.5-2.4-.3-2.4-.3zm4.3.2s-2.8 3.7-2 3.8c.9.2 4.1-2.4 4.2-2.8 0-.5-2.2-1-2.2-1z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M307.8 189.6s-3 3.7-2.1 4 1-.4 1-.4 2.3-2 2.4-2.5-1.2-1.2-1.2-1.2"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M308.7 190.5s-2.1 3.7-1.6 4 1.5-.8 2.4-1.9-.8-2.1-.8-2.1z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M309.3 192.2s-1.3 2.3-1 3 1.6-.5 2.2-1.3-1.2-1.7-1.2-1.7zm.5 6.5s-.8 3.2-.2 3.5c1.3.7 2-2.2 2.2-3.2.1-1.2-2-.3-2-.3z"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M315.7 194.4s-.9-.3-1.5-.3c0 0-1.9-1.4-3-1.6-1-.1 0-.2 0-.2s-.2-2.4-.5-2.6c0 0-.1-2.5-1.7-2.6s-5.1.1-5.7-.1-2.6-1-6.3 0c-3.8 1-11.2 4.3-11.6 4.4-.4 0 8.5-2 11-3l3.3-.5s-2.8 1.4-.2.8 2 0 2 0-.2.6 1.3.3c1.4-.4 1.4 0 1.4 0s1.7.6 3-.2c0 0 .7 2.3 1.6 2.6 0 0 1 2.2 3.1 2.6l1.2.9 1.2.4 1.3-1"/>
|
||||||
|
<path fill="#452c25" stroke="#000" stroke-width=".1" d="M298.3 188s-8.6 3-8.3 3.9c.4.8 9-2.4 10.3-3.2 1.4-1-2-.7-2-.7"/>
|
||||||
|
<path d="m321.8 194.6.5.2-.3-.3-.7-.3.1.2zM315 201l.3.8v-.2q0-.4-.3-.7zm1.2-6.5.9-.2v-.1l-.9.1zm1.7-.2.8-.1v-.1zm-3 1.5v.2zm-.1 4.6v.7-.1z"/>
|
||||||
|
<path fill="#bd8759" d="M316.6 213.9s-1 .7-1 1l-.2.8s.8-.1.7.3c0 0 .3.2.8-.8q.6-1.5 1.1-1.5.6.2 1 .6.2.4.8.3s-.4-.6-.2-.7l.6-.1s-.3-.7-.9-.8q-1-.2-1-.6c.2-.2 1-2.4 1-2.4l-1-1.5-.5 1.3.2 1-1 2.1c0 .1-2.4 1-2.7 1l-.8.9v.5s.3-.4.4-.2q0-.2.6-.2l.1.2.5-.4.1-.1.6-.1"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M316.6 213.9s-1 .7-1 1l-.2.8s.8-.1.7.3c0 0 .3.2.8-.8q.6-1.5 1.1-1.5.6.2 1 .6.2.4.8.3s-.4-.6-.2-.7l.6-.1s-.3-.7-.9-.8q-1-.2-1-.6c.2-.2 1-2.4 1-2.4l-1-1.5-.5 1.3.2 1-1 2.1c0 .1-2.4 1-2.7 1l-.8.9v.5s.3-.4.4-.2q0-.2.6-.2l.1.2.5-.4.1-.1.6-.1"/>
|
||||||
|
<path fill="#bd8759" d="M323.1 209.2v2.1q0 .6-.2 1.1t-.7.5c-.3 0-1 0-1.2.3l-.3.4s.5-.3.6 0l-.2.6.9-.2.8-.3.4.2v1c0 .4 0 1.2.3 1.2l.4-.5.5.5-.1-1.2-.3-1 1.5.6q.2.4.5.5c.1 0 0-.5.2-.5h.4s-.4-.8-1-1l-1.2-.6-.3-.9v-2.8l-1-.2"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M323.1 209v2.3q0 .6-.2 1.1t-.7.5c-.3 0-1 0-1.2.3l-.3.4s.5-.3.6 0l-.2.6.9-.2.8-.3.4.2v1c0 .4 0 1.2.3 1.2l.4-.5.5.5-.1-1.2-.3-1s1.4.5 1.5.7l.5.4c.1 0 0-.5.2-.5h.4s-.4-.8-1-1l-1.2-.6-.3-.9v-2.8z"/>
|
||||||
|
<path fill="#dcddde" d="M315 197.3s0-1.3.3-1.4c0 0 .1-1.2 1.7-1 0 0 .5-.9 1.4-.4 0 0 .8-.4 1.3-.2l1 .7s.7-.1 1 .1.2 1.1.2 1.1.8.6.8 1.1v.9s.3.3.2.7q-.2.8-.4 1c-.1 0 0 1-.3 1.3l-.8.5c-.2 0-.6.6-1 .6-.3 0-.8-.5-.8-.7s-.6-.4-.6-.4-1 1.2-1.8 1-1.1-.8-1.2-1l-.3-1s-.8-.4-.7-.9c0-.4.4-1 .4-1z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m317.8 212-.2-.1m.6-.9-.3-.1m0 .6h.2m5.5.5.5.2m-.2-.6h-.3m.4-.5h-.5m0 1.8.4.1m-.4.5h.2m-.1 1.6h.3m-5.1-2-.3.1m1-.1q-.2.1-.3.4m-4.5.5.4.3m10-1-.3.3m.7-.1v.2"/>
|
||||||
|
<path fill="#d9c0b9" d="M313.4 215h.6s-.3.7-.2.8-.5-.3-.3-.7"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M313.4 215h.6s-.3.7-.2.8-.5-.3-.3-.7z"/>
|
||||||
|
<path fill="#d9c0b9" d="M315.6 215.5s-.7 1 .1 1.5c0 0 0-1 .6-1.1z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M315.6 215.5s-.7 1 .1 1.5c0 0 0-1 .6-1.1z"/>
|
||||||
|
<path fill="#d9c0b9" d="m319.6 214.5-.2-.5.2-.3h.6s.4.9 0 1.3c0 0 0-.5-.2-.5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m319.6 214.5-.2-.5.2-.3h.6s.4.9 0 1.3c0 0 0-.5-.2-.5z"/>
|
||||||
|
<path fill="#d9c0b9" d="M321 213.4h.3l.1.3v.3l-.3.2s-.5-.2-.4.5c0 0-.3-1.2.2-1.3"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M321 213.4h.3l.1.3v.3l-.3.2s-.5-.2-.4.5c0 0-.3-1.2.2-1.3z"/>
|
||||||
|
<path fill="#d9c0b9" d="m323.5 215.9.4-.4.4.2-.3 1.2-.2-.3z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m323.5 215.9.4-.4.4.2-.3 1.2-.2-.3z"/>
|
||||||
|
<path fill="#d9c0b9" d="m326 214.8.5 1s.6-.6-.2-1.5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m326 214.8.5 1s.6-.6-.2-1.5z"/>
|
||||||
|
<path d="m323.3 195.9.2.2v-.1zm.1 2.2.4.7v-.2l-.4-.7zm-.4-1.6.5.6v-.2zm.5 3.5q0 .5-.2.8v.2q.3-.5.2-1.1zm.5 1v.6zm1.8 3.9.1 1.3-.1-1.5zm-.3.8-.4 1 .4-.8zm-10.3-2.9.7 1.2v-.1zm1.8 0-.5-.4v.2c.2 0 .3.4.5.4zm-1 1.8.5 1v-.2l-.5-1zm1.2.4.5.8v-.1q-.1-.5-.5-.8m0 1.3.2.7v-.1zm1.1.3.1.7v-.9.2m-.5 1q0 .5.3 1v-.2zm.2-4.4.8.3v-.1zm.2 1.2 1 .4-1-.6zm.3 1.2.6.6v-.1zm.4 1.6.2.7-.2-.8zm1-4v.5zm2.3-.9-.2.5v.1l.2-.4zm1 0v.9zm.2 1.6v.7-.8m.7 1.5q0 .5-.5.8v.1q.5-.2.5-.8m-2.4-1.5-.2.4v.1zm.3 1.8-.4.4v.1q.3 0 .4-.3zm-.9 1q-.4-.6-.3-1.3v-.1q-.2.8.3 1.5v-.2m-1-1-.1.5v-.6m2.6 1.3q0 .4-.3.7v.1l.3-.6zm1.7.7-.5.7v.2zm.8.8-.2 1.3v.1zm1-1.6v.5zM315 202l.1 1.3v-.2l-.1-1z"/>
|
||||||
|
<path fill="#fff" d="M318.8 196.3c.7-.6 1.7-1.3 2.7-1v-.2c-1-.2-2 .5-2.7 1.1zm3.5.8c-.8-.5-2-.6-2.7 0 .7-.5 2-.3 2.7.2zm-2.8 1.1q.6 1.5.5 3 .1-1.5-.5-3m-1.1.6v3zm1.5-.7q1.6.4 2.4 1.7v-.1a4 4 0 0 0-2.4-1.7zm-.8 1.3-.2 1.8q.3-.8.2-1.8m1.4-.1a3 3 0 0 1 1.2 2v-.1a3 3 0 0 0-1.2-2m-4.3 1.7q.3-.5.8-.7v-.1q-.6.1-.8.7zm1.4-5q-.2-.9-1-1.2v.1q.8.4 1 1.2zm-.8 0-1.5-.2v.1q.7 0 1.5.3zm1.5 0q.1-.9-.4-1.3.5.4.4 1.2m1.8.7q.8-.3 1.5-.2v-.2l-1.5.2zm.9 1.2a3 3 0 0 1 1.7 1v-.3a3 3 0 0 0-1.7-1zm-3.1 2c-.2.6 0 1.5-.9 1.8v.2c.8-.3.7-1.1.9-1.8z"/>
|
||||||
|
<path fill="#fff" d="M319.5 199q.6 1 .5 2.2v.3q.2-1.4-.5-2.7zm1-.1q1.1.2 1.4 1.4v-.2a2 2 0 0 0-1.4-1.5zm0 1q.3.9.1 1.8v.2c.3-.7 0-1.5 0-2.2v.2m-.4-2.6q1.1-.3 2.2.3v-.3q-1.1-.6-2.2-.2zm-1.4-1.5q.2-.9 1.2-1.3v-.2q-1 .3-1.3 1.3zm-.6.4q-.1-1-.7-1.9v.2q.5.8.6 1.8zm-.6.4a2 2 0 0 0-1-1.4v.3q.8.4 1 1.3zm-1-.3-1.2-.5v.2q.7.1 1.2.5zm2.2 3q.2 1-.2 1.9v.2q.5-1.1.2-2.3z"/>
|
||||||
|
<path fill="#fff" d="M319.4 198.9q.4 1 .4 2a6 6 0 0 0-.4-2.3zm.5-1q1 .4 1.7 1v-.3a4 4 0 0 0-1.7-.9zm-1.7-1.4v-1.7zm-1.2-.4-1.2-.8v.2l1.2.9zm-.2 3.3q-.4.3-.6.8v.2l.6-.7zm.9.5q0 .8-.6 1.2v.2q.6-.6.6-1.3s0-.3 0 0m2.1-3.6c.4-.6 1.3-.7 1.9-.9v-.2c-.6.2-1.5.2-2 .8v.3m-4 0-.5-.3v.2l.4.2v-.2m.1 3-.4.3v.2l.4-.2zm.5.1-.6.6v.2l.6-.6zm1 .3c-.1.6-.8 1.2-.3 1.8v-.2c-.3-.5.2-1 .4-1.4zm.7 0q0 .9-.2 1.7v.2l.2-2m1.9-.6q.6.7 1 1.8v-.2a5 5 0 0 0-1-1.9zm.2-.5q1.1.7 2 1.8v-.2q-.9-1.1-2-1.8z"/>
|
||||||
|
<path fill="#fff" d="M320.5 197.8h.8l.6.5.6.4.3.9v-.2l-.2-.7q-.1-.3-.5-.5l-.7-.4-.9-.2z"/>
|
||||||
|
<path fill="#fff" d="m322.3 199.3.2.6v-.1l-.2-.7zm-2-3.6 1.5-.4v-.3l-1.5.4zm-2 .2q0-.3.3-.7l.4-.8v-.3l-.4.7-.4.9zm-1.7 4.6.1 1.3v-1.2s0-.1 0 0m1 .7q-.3.4-.1 1v-.2zm.8-.5-.2.7v.2zs0-.2 0 0m0-1.5v.4zm-.3.2v.3-.7zm-.5-.2-.1.5v.2l.1-.4zm-.4 0-.8.6v.3l.8-.7zm-.6 0-.5.3v.3q.3 0 .5-.3v-.2m-1-.3-.4.2v.2q.3 0 .4-.2zm2.9 0v.4-.6zm.4-.2.1.6v-.7.1m.3 0v.3h.1l-.1-.6zm.4-.2.4.2v-.2l-.4-.2zm.2-.3.6.3v-.3l-.6-.3zm.5-1h.4v-.2l-.4-.1zm-.3-.6.4-.3v-.3l-.4.4zm-.4-.5.2-.5v-.2zm-.3-.2.4-.8v-.2l-.4.7zm-.9-.8v.9zm-1.3.2.2.3v.6l.1-.5-.3-.6zm-.2.5-.6-.4v.2l.6.5zm.8.2q0-1-.2-1.7v.3q.3.5.2 1.2zm1.2-.7v-.2z"/>
|
||||||
|
<path fill="#fff" d="m317.7 196.3.2-1v-.2l-.2 1zm3.5 2q.5 0 .7.6v-.4q-.2-.5-.7-.6zm-1 2v.7h.1v-1 .3m-.8-.5v1.2s0 .2 0 0v-1.4zm-.4.4-.3 1.2v.3s0 .1 0 0l.3-1.2zm.5.2v1.4-1.6zm-2.6.2v.5q.2-.4 0-.8zm1.2-.8v.4-.8zm.3 0h.1v.2-.2l-.1-.5v.2zm.4-.3.1.3v-.3l-.1-.4v.3m.8-.2.5.6v-.4l-.5-.6zm.5-.8.3.6v-.4l-.3-.5zm-4.3-2-.4-.2v.4l.4.3zm1.3-.5v.2zv-.4.2zm1-1v.8q-.2 0 0 .2v.1l.1-1.2v-.1.2m.2 1.5.5-.7v-.3l-.5.6zm.7 0 .2-.3v-.4l-.2.2v.4m1 .2h-.8v.4h.8zm-.3.9 1 .6v-.4l-1-.6z"/>
|
||||||
|
<path fill="#fff" d="m320 198 .8.5v-.4l-.8-.5v.2zm-2-1.4.1-.5v-.2.6m-2.3-1q.3.5.9.8l.6.7s.1-.4 0-.4l-.6-.6-1-.9v.4m.6 1-.4-.4v.3l.4.5zm-.3 2.5-.3.6v.4l.3-.6zm.3.4-.1.6v.3zv-.2zm1 1.8q.3-1 .2-2v.2q0 .8-.2 1.4zm.6-1.5v.9-1.3.2zm.6-.2q.3.3.2 1v.1q.2-.7-.2-1.5z"/>
|
||||||
|
<path fill="#fff" d="m318.6 199.3.3 1.7v-.3l-.2-1.7v.3m.5-.1.6 1v-.3l-.6-1zm1.1-1q-.1-.2-.5-.1v.3q.3 0 .5.2v-.3"/>
|
||||||
|
<path fill="#fff" d="M319.7 198.1h1.1v-.4h-1c-.1 0 0 .4 0 .4m.6-3q-.4.7-.5 1.3v.3s0 .1 0 0q0-.6.5-1.1zm-1.5.4.2 1v-.4l-.2-1zm-.3.5-.3-.5v.4l.2.5v-.3m-.5.2-1.2-.6v.4l1.2.6s.1-.4 0-.4"/>
|
||||||
|
<path fill="#dba05f" d="m318.5 196.2-2.2.4-1.8.2-1.3-.4c-.3 0-1.6-.2-2 .3l-.9.8c-.1.1-.7.6-.7.9q0 .4.4.5c.3 0 .9.6.9.7 0 .3.8.5 1.5.5 1.3 0 2-.7 4-.5 1 .2 3-.7 3.4-1.2q.7-.7.2-1.7c-.3-.6-1.4-.5-1.5-.5"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="m318.5 196.2-2.2.4-1.8.2-1.3-.4c-.3 0-1.6-.2-2 .3l-.9.8c-.1.1-.7.6-.7.9q0 .4.4.5c.3 0 .9.6.9.7 0 .3.8.5 1.5.5 1.3 0 2-.7 4-.5 1 .2 3-.7 3.4-1.2q.7-.7.2-1.7c-.3-.6-1.4-.5-1.5-.5z"/>
|
||||||
|
<path d="m311.4 198.1.1-.1.1-.2.4-.3h.5-.2.2-.2l-.3.1z"/>
|
||||||
|
<path fill="none" d="m311.3 198 .4-.4.6-.3h.3"/>
|
||||||
|
<path fill="none" d="m312 197.4-.4.3q-.1.3-.3.3m.7-.5h.5m-.6 0h.5"/>
|
||||||
|
<path d="m312.4 197.9-.2.1-.1.1-.3.2-.3.1.4-.2-.4.2.4-.1-.3.1h.1s.3 0 .7-.4"/>
|
||||||
|
<path fill="none" d="m312.3 197.8-.3.2-.3.2-.2.1m.4-.1.2-.2.2-.2m-.4.4-.3.1m.4-.1-.3.1"/>
|
||||||
|
<path fill="#c6262c" d="M312.9 196.5s0-.6-.6-.8l-1.3-.2-.5.2-.6.2-.1.2q-.3.3-.3.6c.2.3 0 .3.2.3s-1 .3-.8.8c.3.6.5.4.6.3l.5-.2.7-.7 1-.4h.5z"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M312.9 196.5s0-.6-.6-.8l-1.3-.2-.5.2-.6.2-.1.2q-.3.3-.3.6c.2.3 0 .3.2.3s-1 .3-.8.8c.3.6.5.4.6.3l.5-.2.7-.7 1-.4h.5z"/>
|
||||||
|
<path d="M312.2 197.8q0 .2-.3.3h-.3s.1-.4.3-.4z"/>
|
||||||
|
<path fill="#d9c0b9" d="M308.6 200.2s-.7-1.2 1.2-1.8c0 0 .6.3.8.6 0 0-.4.5-1.5.7 0 0-.5.1-.5.5"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M308.6 200.2s-.7-1.2 1.2-1.8c0 0 .6.3.8.6 0 0-.4.5-1.5.7 0 0-.5.1-.5.5z"/>
|
||||||
|
<path fill="#d9c0b9" d="M308.8 200s.8.2 1.3-.2q.7-.4.7-.1l-.2-.7-1 .5q-.6.1-.8.5"/>
|
||||||
|
<path fill="none" stroke="#000" stroke-width=".1" d="M308.8 200s.8.2 1.3-.2q.7-.4.7-.1l-.2-.7-1 .5q-.6.1-.8.5z"/>
|
||||||
|
<path fill="#7a2e26" d="M311 199.6h.5v-.1zm-.3-2-.1.2v.1zm.5.2.3-.2v-.1q-.3 0-.3.2zm.5.8.2-.2h-.2zm.4-.2.4-.3v-.1zm-.7-.7q0 .2-.3.4v.1q.2-.1.3-.4m.3 2q.6 0 1-.2h-1zm1.6 0q.6-.5 1-1.3a2 2 0 0 1-1 1v.2m.3-.8q.4-.2.6-.8v-.1l-.6.8zm-.2-2 .7.4v-.1l-.7-.4zm6.3 1q.2-.8-.2-1.4.4.6.2 1.2zm-4.8-.7c.3.2 1 .7.8 1.2.3-.5-.4-1-.8-1.3zm2.1 2 .1-.3v.3"/>
|
||||||
|
<path fill="#5e3f17" d="m317.9 199.2.5-.2m-8.5-.3q-.6.1-1 .7.4-.6 1-.6"/>
|
||||||
|
<path fill="#842116" d="M309.7 197.3q.2 0 .3.2v.1-.3l-.3-.1m.7.4-.1-.6v.5m.6-.5q0-.5-.3-.6v.1q.3.2.3.6zm.5-.1q0-.5-.2-.6v.1l.2.3zm-.8-.7c.4-.2 1.4-.3 1.8.2v-.2c-.4-.4-1.4-.3-1.8 0"/>
|
||||||
|
<path fill="#7a2e26" d="M309.5 198.1v-.5s-.1 0 0 .1zm4.7 1.4.3-.5-.3.3zm.7 0 .1-.3v-.1.3m4.3-2.4v.8zm-.5.5v.7q.2-.4 0-.9zm-.4.3"/>
|
||||||
|
<path fill="#452c25" d="M324.3 210.5v.2l.1.1-.1-.4z"/>
|
||||||
|
<path fill="#dcddde" d="M314.4 195c-1.7 0-3.3-1-3.3-1a3 3 0 0 1-2.3-2.4c-.8-.4-1.6-2.5-1.6-2.5-1.3.7-3 0-3 0s0-.4-1.4 0c-1.5.3-1.3-.3-1.3-.3s.6-.6-2 0 .2-.8.2-.8c-.8.2-3.3.4-3.3.4q-1.3.2-2.8.8l-2.3.6-6.8 3-5.5 2.1c.2 0 3.4-2.1 7.5-4a88 88 0 0 1 10.3-3.9 10 10 0 0 1 6.5 0c.6.2 4.2 0 5.7 0 1.6.3 1.8 2.9 1.8 2.9.3.1.4 2.6.4 2.6s-1 0 .1.1c1 .2 2.9 1.7 2.9 1.7h.7s.4-.5.9-.7l1.5-.5h2l2 .3 1.2.3h.5c1-.5 3-1.2 3.4-1.1 0 0 .7-.2 1.1-.5l1.3-1s-.6-4.5 3.9-4l11.5 1.2a44 44 0 0 1 6.5 1.8l5.2 2.4 4 1.7c2.3 1 4 2.4 4 2.4l-3.7-1.8-2.6-.9-3.6-1.7c-3.5-1.6-3.5-1.7-4.9-1.8-1 0 .7 1.2.7 1.2l-4.1-1.7a6 6 0 0 0-2.9-.5 6 6 0 0 1-2.2-.3c-.6-.2-3.8-.3-4.4-.4l-1-.2.2.4-1.5-.3-.5.7s-1.5.3-1.6-.2-1 2.2-1.3 3-2.2.6-2.7 1l-1.6 1h-1.3c-.6 0-.1 0-1 .3 0 0-.9 0-1.2-.2l-1.4-.4-3.5-.2a5 5 0 0 0-2.7 1.1"/>
|
||||||
|
<path fill="#e7e7e7" stroke="#000" stroke-width=".1" d="M314.4 195c-1.7 0-3.3-1-3.3-1a3 3 0 0 1-2.3-2.4c-.8-.4-1.6-2.5-1.6-2.5-1.3.7-3 0-3 0s0-.4-1.4 0c-1.5.3-1.3-.3-1.3-.3s.6-.6-2 0 .2-.8.2-.8c-.8.2-3.3.4-3.3.4q-1.3.2-2.8.8l-2.3.6-6.8 3-5.5 2.1c.2 0 3.4-2.1 7.5-4a88 88 0 0 1 10.3-3.9 10 10 0 0 1 6.5 0c.6.2 4.2 0 5.7 0 1.6.3 1.8 2.9 1.8 2.9.3.1.4 2.6.4 2.6s-1 0 .1.1c1 .2 2.9 1.7 2.9 1.7h.7s.4-.5.9-.7l1.5-.5h2l2 .3 1.2.3h.5c1-.5 3-1.2 3.4-1.1 0 0 .7-.2 1.1-.5l1.3-1s-.6-4.5 3.9-4l11.5 1.2a44 44 0 0 1 6.5 1.8l5.2 2.4 4 1.7c2.3 1 4 2.4 4 2.4l-3.7-1.8-2.6-.9-3.6-1.7c-3.5-1.6-3.5-1.7-4.9-1.8-1 0 .7 1.2.7 1.2l-4.1-1.7a6 6 0 0 0-2.9-.5 6 6 0 0 1-2.2-.3c-.6-.2-3.8-.3-4.4-.4l-1-.2.2.4-1.5-.3-.5.7s-1.5.3-1.6-.2-1 2.2-1.3 3-2.2.6-2.7 1l-1.6 1h-1.3c-.6 0-.1 0-1 .3 0 0-.9 0-1.2-.2l-1.4-.4c-.5-.2-3.1-.2-3.5-.2a5 5 0 0 0-2.7 1.1"/>
|
||||||
|
<path fill="#452c25" d="M314.6 194.4s-.2.2-.2.5v.2"/>
|
||||||
|
<path fill="#574f4c" d="m323.3 194 .7.5-.1-.1-.7-.6v.1"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 100 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bq" viewBox="0 0 640 480">
|
||||||
|
<path fill="#21468b" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fff" d="M0 0h640v320H0z"/>
|
||||||
|
<path fill="#ae1c28" d="M0 0h640v160H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 221 B |
@@ -0,0 +1,45 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-br" viewBox="0 0 640 480">
|
||||||
|
<g stroke-width="1pt">
|
||||||
|
<path fill="#229e45" fill-rule="evenodd" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#f8e509" fill-rule="evenodd" d="m321.4 436 301.5-195.7L319.6 44 17.1 240.7z"/>
|
||||||
|
<path fill="#2b49a3" fill-rule="evenodd" d="M452.8 240c0 70.3-57.1 127.3-127.6 127.3A127.4 127.4 0 1 1 452.8 240"/>
|
||||||
|
<path fill="#ffffef" fill-rule="evenodd" d="m283.3 316.3-4-2.3-4 2 .9-4.5-3.2-3.4 4.5-.5 2.2-4 1.9 4.2 4.4.8-3.3 3m86 26.3-3.9-2.3-4 2 .8-4.5-3.1-3.3 4.5-.5 2.1-4.1 2 4.2 4.4.8-3.4 3.1m-36.2-30-3.4-2-3.5 1.8.8-3.9-2.8-2.9 4-.4 1.8-3.6 1.6 3.7 3.9.7-3 2.7m87-8.5-3.4-2-3.5 1.8.8-3.9-2.7-2.8 3.9-.4 1.8-3.5 1.6 3.6 3.8.7-2.9 2.6m-87.3-22-4-2.2-4 2 .8-4.6-3.1-3.3 4.5-.5 2.1-4.1 2 4.2 4.4.8-3.4 3.2m-104.6-35-4-2.2-4 2 1-4.6-3.3-3.3 4.6-.5 2-4.1 2 4.2 4.4.8-3.3 3.1m13.3 57.2-4-2.3-4 2 .9-4.5-3.2-3.3 4.5-.6 2.1-4 2 4.2 4.4.8-3.3 3.1m132-67.3-3.6-2-3.6 1.8.8-4-2.8-3 4-.5 1.9-3.6 1.7 3.8 4 .7-3 2.7m-6.7 38.3-2.7-1.6-2.9 1.4.6-3.2-2.2-2.3 3.2-.4 1.5-2.8 1.3 3 3 .5-2.2 2.2m-142.2 50.4-2.7-1.5-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2M419 299.8l-2.2-1.1-2.2 1 .5-2.3-1.7-1.6 2.4-.3 1.2-2 1 2 2.5.5-1.9 1.5"/>
|
||||||
|
<path fill="#ffffef" fill-rule="evenodd" d="m219.3 287.6-2.7-1.5-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2"/>
|
||||||
|
<path fill="#ffffef" fill-rule="evenodd" d="m219.3 287.6-2.7-1.5-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2m42.3 3-2.6-1.4-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .5-2.3 2.1m-4.8 17-2.6-1.5-2.7 1.4.6-3-2.1-2.3 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2m87.4-22.2-2.6-1.6-2.8 1.4.6-3-2-2.3 3-.3 1.4-2.7 1.2 2.8 3 .5-2.2 2.1m-25.1 3-2.7-1.5-2.7 1.4.6-3-2-2.3 3-.3 1.4-2.8 1.2 2.9 3 .5-2.2 2.1m-68.8-5.8-1.7-1-1.7.8.4-1.9-1.3-1.4 1.9-.2.8-1.7.8 1.8 1.9.3-1.4 1.3m167.8 45.4-2.6-1.5-2.7 1.4.6-3-2.1-2.3 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2m-20.8 6-2.2-1.4-2.3 1.2.5-2.6-1.7-1.8 2.5-.3 1.2-2.3 1 2.4 2.5.4-1.9 1.8m10.4 2.3-2-1.2-2.1 1 .4-2.3-1.6-1.7 2.3-.3 1.1-2 1 2 2.3.5-1.7 1.6m29.1-22.8-2-1-2 1 .5-2.3-1.6-1.7 2.3-.3 1-2 1 2.1 2.1.4-1.6 1.6m-38.8 41.8-2.5-1.4-2.7 1.2.6-2.8-2-2 3-.3 1.3-2.5 1.2 2.6 3 .5-2.3 1.9m.6 14.2-2.4-1.4-2.4 1.3.6-2.8-1.9-2 2.7-.4 1.2-2.5 1.1 2.6 2.7.5-2 2m-19-23.1-1.9-1.2-2 1 .4-2.2-1.5-1.7 2.2-.2 1-2 1 2 2.2.4-1.6 1.6m-17.8 2.3-2-1.2-2 1 .5-2.2-1.6-1.7 2.3-.2 1-2 1 2 2.1.4-1.6 1.6m-30.4-24.6-2-1.1-2 1 .5-2.3-1.6-1.6 2.2-.3 1-2 1 2 2.2.5-1.6 1.5m3.7 57-1.6-.9-1.8.9.4-2-1.3-1.4 1.9-.2.9-1.7.8 1.8 1.9.3-1.4 1.3m-46.2-86.6-4-2.3-4 2 .9-4.5-3.2-3.3 4.5-.6 2.2-4 1.9 4.2 4.4.8-3.3 3.1"/>
|
||||||
|
<path fill="#fff" fill-rule="evenodd" d="M444.4 285.8a125 125 0 0 0 5.8-19.8c-67.8-59.5-143.3-90-238.7-83.7a125 125 0 0 0-8.5 20.9c113-10.8 196 39.2 241.4 82.6"/>
|
||||||
|
<path fill="#309e3a" d="m414 252.4 2.3 1.3a3 3 0 0 0-.3 2.2 3 3 0 0 0 1.4 1.7q1 .8 2 .7.9 0 1.3-.7l.2-.9-.5-1-1.5-1.8a8 8 0 0 1-1.8-3 4 4 0 0 1 2-4.4 4 4 0 0 1 2.3-.2 7 7 0 0 1 2.6 1.2q2.1 1.5 2.6 3.2a4 4 0 0 1-.6 3.3l-2.4-1.5q.5-1 .2-1.7-.2-.8-1.2-1.4a3 3 0 0 0-1.8-.7 1 1 0 0 0-.9.5q-.3.4-.1 1 .2.8 1.6 2.2t2 2.5a4 4 0 0 1-.3 4.2 4 4 0 0 1-1.9 1.5 4 4 0 0 1-2.4.3q-1.3-.3-2.8-1.3-2.2-1.5-2.7-3.3a5 5 0 0 1 .6-4zm-11.6-7.6 2.5 1.3a3 3 0 0 0-.2 2.2 3 3 0 0 0 1.4 1.6q1.1.8 2 .6.9 0 1.3-.8l.2-.8q0-.5-.5-1l-1.6-1.8q-1.7-1.6-2-2.8a4 4 0 0 1 .4-3.1 4 4 0 0 1 1.6-1.4 4 4 0 0 1 2.2-.3 7 7 0 0 1 2.6 1q2.3 1.5 2.7 3.1a4 4 0 0 1-.4 3.4l-2.5-1.4q.5-1 .2-1.7-.4-1-1.3-1.4a3 3 0 0 0-1.9-.6 1 1 0 0 0-.8.5q-.3.4-.1 1 .3.8 1.7 2.2 1.5 1.5 2 2.4a4 4 0 0 1 0 4.2 4 4 0 0 1-1.8 1.6 4 4 0 0 1-2.4.3 8 8 0 0 1-2.9-1.1 6 6 0 0 1-2.8-3.2 5 5 0 0 1 .4-4m-14.2-3.8 7.3-12 8.8 5.5-1.2 2-6.4-4-1.6 2.7 6 3.7-1.3 2-6-3.7-2 3.3 6.7 4-1.2 2zm-20.7-17 1.1-2 5.4 2.7-2.5 5q-1.2.3-3 .2a9 9 0 0 1-3.3-1 8 8 0 0 1-3-2.6 6 6 0 0 1-1-3.5 9 9 0 0 1 1-3.7 8 8 0 0 1 2.6-3 6 6 0 0 1 3.6-1.1q1.4 0 3.2 1 2.4 1.1 3.1 2.8a5 5 0 0 1 .3 3.5l-2.7-.8a3 3 0 0 0-.2-2q-.4-.9-1.6-1.4a4 4 0 0 0-3.1-.3q-1.5.5-2.6 2.6t-.7 3.8a4 4 0 0 0 2 2.4q.8.5 1.7.5h1.8l.8-1.6zm-90.2-22.3 2-14 4.2.7 1.1 9.8 3.9-9 4.2.6-2 13.8-2.7-.4 1.7-10.9-4.4 10.5-2.7-.4-1.1-11.3-1.6 11zm-14.1-1.7 1.3-14 10.3 1-.2 2.4-7.5-.7-.3 3 7 .7-.3 2.4-7-.7-.3 3.8 7.8.7-.2 2.4z"/>
|
||||||
|
<g stroke-opacity=".5">
|
||||||
|
<path fill="#309e3a" d="M216.5 191.3q0-2.2.7-3.6a7 7 0 0 1 1.4-1.9 5 5 0 0 1 1.8-1.2q1.5-.5 3-.5 3.1.1 5 2a7 7 0 0 1 1.6 5.5q0 3.3-2 5.3a7 7 0 0 1-5 1.7 7 7 0 0 1-4.8-2 7 7 0 0 1-1.7-5.3"/>
|
||||||
|
<path fill="#f7ffff" d="M219.4 191.3q0 2.3 1 3.6t2.8 1.3a4 4 0 0 0 2.8-1.1q1-1.2 1.1-3.7.1-2.4-1-3.6a4 4 0 0 0-2.7-1.3 4 4 0 0 0-2.8 1.2q-1.1 1.2-1.2 3.6"/>
|
||||||
|
</g>
|
||||||
|
<g stroke-opacity=".5">
|
||||||
|
<path fill="#309e3a" d="m233 198.5.2-14h6q2.2 0 3.2.5 1 .3 1.6 1.3c.6 1 .6 1.4.6 2.3a4 4 0 0 1-1 2.6 5 5 0 0 1-2.7 1.2l1.5 1.2q.6.6 1.5 2.3l1.7 2.8h-3.4l-2-3.2-1.4-2-.9-.6-1.4-.2h-.6v5.8z"/>
|
||||||
|
<path fill="#fff" d="M236 190.5h2q2.1 0 2.6-.2.5-.1.8-.5.4-.6.3-1 0-.9-.4-1.2-.3-.4-1-.6h-2l-2.3-.1z"/>
|
||||||
|
</g>
|
||||||
|
<g stroke-opacity=".5">
|
||||||
|
<path fill="#309e3a" d="m249 185.2 5.2.3q1.7 0 2.6.3a5 5 0 0 1 2 1.4 6 6 0 0 1 1.2 2.4q.4 1.4.3 3.3a9 9 0 0 1-.5 3q-.6 1.5-1.7 2.4a5 5 0 0 1-2 1q-1 .3-2.5.2l-5.3-.3z"/>
|
||||||
|
<path fill="#fff" d="m251.7 187.7-.5 9.3h3.8q.8 0 1.2-.5.5-.4.8-1.3t.4-2.6l-.1-2.5a3 3 0 0 0-.8-1.4l-1.2-.7-2.3-.3z"/>
|
||||||
|
</g>
|
||||||
|
<g stroke-opacity=".5">
|
||||||
|
<path fill="#309e3a" d="m317.6 210.2 3.3-13.6 4.4 1 3.2 1q1.1.6 1.6 1.9t.2 2.8q-.3 1.2-1 2a4 4 0 0 1-3 1.4q-1 0-3-.5l-1.7-.5-1.2 5.2z"/>
|
||||||
|
<path fill="#fff" d="m323 199.6-.8 3.8 1.5.4q1.6.4 2.2.3a2 2 0 0 0 1.6-1.5q0-.7-.2-1.3a2 2 0 0 0-1-.9l-1.9-.5-1.3-.3z"/>
|
||||||
|
</g>
|
||||||
|
<g stroke-opacity=".5">
|
||||||
|
<path fill="#309e3a" d="m330.6 214.1 4.7-13.2 5.5 2q2.2.8 3 1.4.8.7 1 1.8c.2 1.1.2 1.5 0 2.3q-.6 1.5-1.8 2.2-1.2.6-3 .3.6.7 1 1.6l.8 2.7.6 3.1-3.1-1.1-1-3.6-.7-2.4-.6-.8q-.3-.4-1.3-.7l-.5-.2-2 5.6z"/>
|
||||||
|
<path fill="#fff" d="m336 207.4 1.9.7q2 .7 2.5.7t.9-.3q.5-.3.6-.9.3-.6 0-1.2l-.8-.9-2-.7-2-.7-1.2 3.3z"/>
|
||||||
|
</g>
|
||||||
|
<g stroke-opacity=".5">
|
||||||
|
<path fill="#309e3a" d="M347 213.6a9 9 0 0 1 1.7-3.2l1.8-1.5 2-.7q1.5-.1 3.1.4a7 7 0 0 1 4.2 3.3q1.2 2.4.2 5.7a7 7 0 0 1-3.4 4.5q-2.3 1.3-5.2.4a7 7 0 0 1-4.2-3.3 7 7 0 0 1-.2-5.6"/>
|
||||||
|
<path fill="#fff" d="M349.8 214.4q-.7 2.3 0 3.8c.7 1.5 1.2 1.6 2.3 2q1.5.5 3-.4 1.4-.8 2.1-3.2.8-2.2 0-3.7a4 4 0 0 0-2.2-2 4 4 0 0 0-3 .3q-1.5.8-2.2 3.2"/>
|
||||||
|
</g>
|
||||||
|
<g stroke-opacity=".5">
|
||||||
|
<path fill="#309e3a" d="m374.3 233.1 6.4-12.4 5.3 2.7a10 10 0 0 1 2.7 1.9q.8.7.8 1.9c0 1.2 0 1.5-.4 2.2a4 4 0 0 1-2 2q-1.5.4-3.1-.2.6 1 .8 1.7.3.9.4 2.8l.2 3.2-3-1.5-.4-3.7-.3-2.5-.5-1-1.2-.7-.5-.3-2.7 5.2z"/>
|
||||||
|
<path fill="#fff" d="m380.5 227.2 1.9 1q1.8 1 2.3 1t1-.2q.4-.2.7-.8t.2-1.2l-.7-1-1.8-1-2-1z"/>
|
||||||
|
</g>
|
||||||
|
<g stroke-opacity=".5">
|
||||||
|
<path fill="#309e3a" d="M426.1 258.7a9 9 0 0 1 2.5-2.6 7 7 0 0 1 2.2-.9 6 6 0 0 1 2.2 0q1.5.3 2.8 1.2a7 7 0 0 1 3 4.4q.4 2.6-1.4 5.5a7 7 0 0 1-4.5 3.3 7 7 0 0 1-5.2-1.1 7 7 0 0 1-3-4.4q-.4-2.7 1.4-5.4"/>
|
||||||
|
<path fill="#fff" d="M428.6 260.3q-1.4 2-1.1 3.6a4 4 0 0 0 1.6 2.5q1.5 1 3 .6t2.9-2.4q1.4-2.1 1.1-3.6t-1.6-2.6c-1.4-1.1-2-.8-3-.5q-1.5.3-3 2.4z"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#309e3a" d="m301.8 204.5 2.3-9.8 7.2 1.7-.3 1.6-5.3-1.2-.5 2.2 4.9 1.1-.4 1.7-4.9-1.2-.6 2.7 5.5 1.3-.4 1.6z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bs" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="bs-a">
|
||||||
|
<path fill-opacity=".7" d="M-12 0h640v480H-12z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g fill-rule="evenodd" clip-path="url(#bs-a)" transform="translate(12)">
|
||||||
|
<path fill="#fff" d="M968.5 480h-979V1.8h979z"/>
|
||||||
|
<path fill="#ffe900" d="M968.5 344.5h-979V143.3h979z"/>
|
||||||
|
<path fill="#08ced6" d="M968.5 480h-979V320.6h979zm0-318.7h-979V2h979z"/>
|
||||||
|
<path fill="#000001" d="M-11 0c2.3 0 391.8 236.8 391.8 236.8L-12 479.2z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 557 B |
@@ -0,0 +1,89 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bt" viewBox="0 0 640 480">
|
||||||
|
<path fill="#ffd520" d="M.1 0h640.1v480H.1z"/>
|
||||||
|
<path fill="#ff4e12" d="M.1 480h640.1V0z"/>
|
||||||
|
<g stroke="#000" stroke-width=".5">
|
||||||
|
<g fill="#fff" stroke-width=".4">
|
||||||
|
<path d="M345.4 150c-4-1.3-6.6.7-6.4 5.9 0 5.1 2.8 8 6.8 6.1z"/>
|
||||||
|
<path d="M348.9 140.4c-3.3-2.6-6.4-1.5-8 3.4s.1 8.6 4.5 8z"/>
|
||||||
|
<path d="M354.4 131c-2.8-3-6-2.4-8.4 2.2-2.3 4.6-1.3 8.5 3.2 8.7zm-3.6 45.5c-4.9 1.8-5.4 8.5-2.3 12.6 3 4.1 8.7 4.9 11.8 0z"/>
|
||||||
|
<path d="M345.1 162.3c-4.6-1.5-8.8 4.7-9.5 10.3-.9 7-11 9.4-5.4 20.1 1.2-6.8 5.7-10.6 9.3-10.8s9-1 11.3-5.4zm14.7 27.6c-5.4 1.3-6.2 8.5-2.3 13.6 3.3 4.4 13.7 3.4 13.4-1zm15.3 43.4c.3-4.7-7.2-6.5-10.8-5.6s-10.5-.1-12-4c-1.4 3.1.5 6.5 5.7 8.2 4 1.2 3.9 4 2.7 5.4 3 .5 11.7.5 14.4-4z"/>
|
||||||
|
<path d="M370.9 203.7c-5.3-2.4-8.4 1.2-10.4 4.6-3 5-12.1-1.4-15.2 5.3 4.2-1.8 8.4 2 10.4 3.3 5.7 3.7 16.6 2.6 18.2-6.2z"/>
|
||||||
|
<path d="M374 209.8c-5.3 4-7.4 8.8-7.2 12 .1 3.2 4.6 10.3 9.5 10.7 2.8-5.8 4.3-18-2.3-22.7zm-22 24.9c0-2 2.8-2.7 4.8-2 1.9.6 4.9 2.5 3.8 4.6zM323 224c-.5-2.3 3.2-6.3 8.2-4.1s5.7 6.4 3.6 8.1z"/>
|
||||||
|
<path d="M335.2 228.4c-.4-1.3 3.3-3.9 9.4-2.4 6.2 1.6 7.7 5.6 7.5 8.7zm-12.9-4.3c3.3-2.4 2-7-.9-8.5-5.2-2.6-3.3-9.2-6.7-10.5-3.3-1.3-6.5-3.6-6.7-6-1.6 3.3-.6 6.2 1.7 8.3 2.3 2-1.8 10.4 1.2 12.6zm-69-1.8c-2.7-4.2-9.1-3.5-11.8-.5s-2.3 7.3.2 9.3zm15.5-6.3c-1-5.7-7.9-6.3-11.6-4.9-3.8 1.4-6.3 7.1-3.9 11.2z"/>
|
||||||
|
<path d="M279 215.2c2.5-4.7-2.3-11.8-7.7-12.8-4.5-1-9.8-.9-11.6-5.3-1 3.8 1.8 6.3 5.2 8.5 3.3 2.2-.6 7.8 4.8 11z"/>
|
||||||
|
<path d="M278.6 215.4c-1.2-3.4 1.1-8 5.7-7.6 4.7.3 7.3 3.6 5.3 7.9z"/>
|
||||||
|
<path d="M288.9 215.7c-.7-3.6 2.2-7.8 6.8-6.8s6.6 4.7 4 8.5l-10.8-1.8z"/>
|
||||||
|
<path d="M299 217.3c-.4-3.6 2.7-7.6 7.2-6.4s6.4 5 3.6 8.7l-10.7-2.3zm-77.6 59c-8.7 0-10.8 2-12 11-1.6 11 13.5 12.3 12-11z"/>
|
||||||
|
<path d="M225.2 264.7c-13.2-5-20.4 16-33.6 12.2 4.7 7.5 16.1 0 20.4.8 7.2 1.3 22.8-1.4 13.2-13zm-8.6 28.7c-6.6-3-13.6 7-12.4 11.5 1.7 5.5 16.7 1 12.4-11.5zM186 336.7c3.6 1 8 3 7.2 10s-14 21.1-25.8 22c-11.8.7-16 15-26.3 11 9.6-1.8 9.6-12.6 17-16-5.4-2-8.2 10.3-15.2 10.3s-10.3 11.1-18.8 10.3-9.3 13.5-26.4 13.7c-13 .1-29 15.3-34.9 8.7 12.7-1.8 17.8-8.8 25.3-16.5 12-12.3 25.7-6.8 30.4-17.7a32 32 0 0 1-18.3 5.4c-8-.2-16.6 12.6-25.5 7 5.1-.7 8.5-2.9 13.9-8.6 5.5-5.8 13.7-2 20.1-8 10-9.2 18.7-1.5 28.3-13-2.7-1.4-8.5-.5-13.9 2.4-5.4 2.8-12.3-2-18.5 1.4.7-7.6 15.2-3.3 24.2-8.5 10.2-6 18.6-4.2 26.6-3.5-11.2 0-15.5-10.7-31-7.6-6.7 1.4-12.1-9.3-18.8-3.9.2-4 7.2-7.2 14.4-3.3s10.1-3.4 24.5 5.7c6 3.7 16.1-2.4 22.5 1.6-.8-2.4-4.5-4-8.8-3.6 2.7-5.6 20.2-4.9 27.8.7z"/>
|
||||||
|
<path d="M197.4 328.3c-5.6-4.4-13.5.9-19-1.2 0 3.6 1.9 9 7.8 11.1 1.8-1.3 10-8.4 11.2-9.9z"/>
|
||||||
|
<path d="M206.3 315.8c-8.9-4.5-10.4 6.7-17.4 4.4.3 3.2 2.9 7.2 8.5 8.1z"/>
|
||||||
|
<path d="M211.6 305.6c-13.1-5.1-14.8 7.5-22.5 5 1.8 4.4 12.8 6.8 18.5 5.2zm18-55.2c-3.5-5-10.8-1-12 4.9-1.1 5.8 1.7 13.9 6.6 12z"/>
|
||||||
|
<path d="M238.5 235c-6-1.5-13-.8-12.2 5-2.4 1.1-3 8.7 3.3 10.4z"/>
|
||||||
|
<path d="M242.5 231c-6-7-12.5-6.9-16.2-4-6.9 5.5-13.5 2.4-13.7 7.8 4.1-3.2 7.8.6 11-.5 3.4-1.2 5.9 5.3 15.2 2.6zm-14.3 103.9c.9 1.7 6.4 2.4 9.1-.4 3.6-3.8-.3-14.2-6-15-5.7-.7-6.2 11.8-3.1 15.4z"/>
|
||||||
|
<path d="M221.8 335.1c8 3 11.5-3.7 7.2-8a80 80 0 0 1-7.2 8z"/>
|
||||||
|
<path d="M191.4 346.2c-1.6 4.7-9.6 5.4-18.6 19.8s-17.6 8.4-19.8 18.3c10.8-8.7 19.3-3 25.8-11.6 9.7-13.1 17.8-11.2 21.6-20 5.4-12.7 29-12.4 30.4-32.3-8-1.5-33.3 19.9-39.4 25.8zm203.2-194.7c10.3 3.4 10.5 16.7 22.4 21.1s13 15 22.7 12.4c-9-2.5-8.4-12.9-17.8-15.5-11-3-15.2-19.8-24-22.4m44 74.3c1.8 4.2 1.5 11.5-5 13.4 3.5 2.2 8.7.2 11.5-4.6-4.2 9.4-1.4 17.9 5.3 19.6-3.2-6.6 4-9.7 1.7-14 4.2 1.9 8 7.7 7.8 11.3 5.6-6.2-4-14.5-2.3-20.3zM375 287.6c-6.3-5.5-9 1.5-12-1-3-2.2-6.8-2.5-8.3-.3 5.4.2 2.8 4.4 13.3 5.4-10.5.7-8.6 12.5-15.6 11.9 7.4 7 11.3-6.3 17.4-4.1-1.8.5 2.8 4.7-.4 10.4 5.2-.1 7.3-7.3 8-11zm-139 60.2c-2.2-2-9-2.9-11.5-1.3-2.6 1.5-1.7 2 1.4 2.2s7 5.2.4 5.5c-3.1.1-2 7.6-8.5 8.1 2.6 3.2 10.2 1.1 12.9-2.4-.5 2.9 3.3 5.5 1.8 9 4.7.5 2-9.7 9.5-9.2-3 .4-1.8 7.4 3.6 5.6-3.2 1.5-1.5 5.3 2 4.4-2.2.7-3 3.7.2 5.4 3.1-4.3-.4-19.5-11.7-27.3zm280.6-142.1a17.9 17.9 0 1 0 0-35.7 17.9 17.9 0 0 0 0 35.7z"/>
|
||||||
|
<path d="M423.4 227.2c5.5-5.1 13.7-7.7 19.4-3.8 5.6 3.8 24.4 8.4 33.7 2s13.7-9.8 17.8-9q4.5 6.8 11.4 7.2c1.4 1.6 6.5 2.8 9.3 2.5 4.1 1 9.1-.3 13.1-4.7 6.2 1 12-3.7 14.2-10.7 6.6-.7 7-8 2.8-13-3.8-.7-.9-13.8-14.9-11.2 6 3.6 1.4 10.8 6.3 14.2-3.3 0-7.7 1.4-8.7 6.4 1.3-3.4-.2-5.8-1-6.5 0-3-6.5-10.3-12.7-7.6 4.4 1 2 8 5.2 10.8a8 8 0 0 0-6.2 3.3c-1.7-3-7.6-6-11.2-6.3 0-1-.2-3-.7-4.1-1.6-3.1-3-6.8-2.3-11.5a48 48 0 0 0-7.2 11.4c-4.9-3.4-17 1.5-22.7 2.8s-24.7-1.8-29-6.4a49 49 0 0 0-21-9.8c-11-3.2-11-15.2-23.1-23.5-.3 15 22.4 62.4 27.5 67.5zM297.2 341.1a17.3 17.3 0 1 0 0-34.6 17.3 17.3 0 0 0 0 34.6z"/>
|
||||||
|
<path d="M256 327.8c3.5 4.5 9.4 4.2 11.9 3.9 2 5.4 8.6 5.2 11.4 8.2s12.5 2.7 15.3 1c-2.5-.2-5.9-1.8-9-4.5-4-3.2-2.2-9.8-5.2-12a11 11 0 0 0 2.2-8.6c2.4-1.4 4.2-3.7 4.4-4.9a15 15 0 0 0 9.6-4.1c2.2 2 7.7-.6 10.7 2.8.6-8.5-7.5-13-13-10.1-2.1-1.2-7.9-.4-9 1.1-1.7-.8-6.8 1.8-9 3.5 2.5-1.4 2.9-5.7 1.9-7.2 2.2-1 4.6-3.9 4.9-6 3 .5 7.7-1.6 9.7-1.1-3.3-4.4-8.8-6-14.5-5.5-5.9.3-8.4 4.4-9.2 8.8-3.4 2.1-4.6 9-3.3 11.5-2 0-3.9 1.9-4.6 3a27 27 0 0 0-9.4-2m1.3-7c-1.2-3.4.3-6.4 1.1-9 2-6.8.8-8.6-5.3-7.7a47 47 0 0 0 4.2 16.7z"/>
|
||||||
|
<path d="M248.6 282.3c1.6 1.6 7 2.3 7.6-2.6.7-5.6-1.6-7.8-6.5-5.6-.4 1.3-.8 6.5-1 8.2z"/>
|
||||||
|
<path d="M249.8 273.9c2 .8 6.5 2.5 9-2.3 2-4-.7-7-5-6.8-1 1.2-3 5.3-4 9.1z"/>
|
||||||
|
<path d="M253.6 264.4c.5 1.6 5.8 6.7 9.6 3 3.9-3.7 3.9-9.3-1.9-11.3-1.5.2-6.2 5.6-7.7 8.3z"/>
|
||||||
|
<path d="M261.3 256c1.1 3.3 4.8 8.8 11.5 6.3s3.8-11 .7-12.7a33 33 0 0 0-12.2 6.5z"/>
|
||||||
|
<path d="M273.5 249.6c-.5 2.9 0 10.6 9.2 10.5 9.1-.2 6.6-10.9 4.2-12.4-3.7 0-10 .1-13.4 2z"/>
|
||||||
|
<path d="M287.3 248c-1 2.3-3.3 16.7 14.6 12.7 2.3-.5 8.3-13.8-14.6-12.6z"/>
|
||||||
|
<path d="M297.1 249.4c-1.8 1.8 2.8 16.3 15 13.9 12-2.5 1.9-16.3-15-14z"/>
|
||||||
|
<path d="M307.4 251.6c-2 4 1 15.8 15.9 15.8 13.5 0-.7-15.6-15.9-15.8z"/>
|
||||||
|
<path d="M319.1 255c-1 2.3-2.1 14.8 15.5 15.9 12.7.8 9.6-17.3-15.5-15.9z"/>
|
||||||
|
<path d="M338 260.3c-2.1 3.9-4.4 13.5 14.9 14.3 12.3.5 4.7-14-14.8-14.3z"/>
|
||||||
|
<path d="M354.1 263.3c-2.8 3.8-.7 11.4 6.5 12.8 9 1.8 10.3-6.7 4.1-10.8-6.2-4-10.6-2-10.6-2z"/>
|
||||||
|
<path d="M363 265c-2.1 3.7-.9 12.4 12.8 12.4 2.8 0 13.6-11-12.9-12.3zM257.1 433a20 20 0 1 0 0-40 20 20 0 0 0 0 40z"/>
|
||||||
|
<path d="M404.1 141.7a35 35 0 0 0-5.4 8c-6.7 20 11.2 35 21.6 56.7a63 63 0 0 1-5.6 60.7c-4.4 5.8-3.1 7.5-8.8 13.4-2.2 2.3-4.6 5.2-3.8 13.4 3.6-1.3 8.7 2 9.7 5 2.6-1.4 6.2-.9 7.5.7 4.4-2 8-1 11.9 3 3.3-.4 7 0 10.3 3.7 1.8-3.6 5.4-5 8-4.1-.3-4.7 4.3-8 8.4-6.2a7.6 7.6 0 0 1 9.8-9c4.7-3.6 14-3.9 18.6 1.5-8.3-2.3-8 6.5-15 5.7 1.8 5.1-2.8 8.1-7.4 9.8 3-1.4 6.2-3.1 7.2-1.3 2.6-2.3 7.7-1.4 9-.3 3.4-1 6.7-.2 8.2 3.9 4.7 2.8 7.8 10 4.4 15.4-1-5.6-4.9-5.4-6.4-7.7-3.6 1.3-7.2 1.3-8.3-1-2 2-9 3.9-12 .8-1.2 4.6-5.2 8.5-9.9 8.5 1.3 3.6-2.3 9.7-5.1 12.8 4.4 2.3 3 7.5 2 10.6 6.8 1 1 7 12.7 10.8-5.7 1.8-16.8 0-18.3-7-5.7-.2-9.5-5.9-9.3-11.8-4.4-4.1-5-10 1-14.2-5.1 1.6-8-6.7-15.4-3.3-3.7 1.7-13.5-1.2-13.4-4.6-1.5 2.5-11 1.5-12.2-2.9-3.1 1.7-10.3-1.1-10.2-5.4-4 1.8-9.4-1.4-9.1-5.5-3.8-.5-4.2-3.9-4-6.7-3.3-1.6-2.4-4.8-1-8.6-2.4-2.6-1.4-6.2.4-9.6-2.5-2.6-2-5.6-1.3-9.3-12.3-1-27.8-4-63.3-14.9-53.6-16.5-68 22.2-56.2 46.4 13.7 28-1.5 34 3.1 54.8 5 1 7.5 5.2 7.2 9.6 3 .1 5 2.8 4 8a9 9 0 0 1 7.6 2.3c1.8-3.4 7.8-4.2 10.8-.3 6.7-.5 10.1 5 9.8 11.6a18 18 0 0 1-1.5 19.3c.4-2.7 0-6.5-.1-8.9-.3-4.2-6.2-5.1-5.6-8.6-3 .3-6-1.4-7-3.7a7 7 0 0 1-6.6 1.3c3.4 1.5 6.2 7.7 5.1 11.8 1.8 3.1 1.4 8.8-.7 11.2-1 5-5 6.8-10 4.6 2.9-1.8 3.9-5 3.8-7.7a10 10 0 0 1-2.9-6.3c-5 .8-12-3.5-13.2-5.2a20 20 0 0 0-20 20.1c-.6-4.1-5.8-8.2-5.1-11.7-3.1-9.5 1.3-18.4 13.9-20.2-1.6-3.6 3.8-7.3 1.8-11.4a97 97 0 0 0-14.7-20.1c4.4-7.5 3-17.5.5-23.7-3.7-8.9-7.2-6.7-20.3 7.7-21.4 23.5-50 17-75.2 32.5-6.7 4-13.4 5.6-6.2-1.6s26.2-14.4 38.6-20.6c23.2-11.6 42.8-30.9 50.5-68.5 18.1-88.4 85-59.2 127.2-42.8 39.7 15.5 32.5-19.5 12.4-40.7-24.2-25.3-19.3-45.3-8-61.3 20.3-2.8 59.4 4.3 51.5 11.1z"/>
|
||||||
|
<path d="M475.9 358.8a22 22 0 1 0 0-44.1 22 22 0 0 0 0 44z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="none" stroke-width=".4">
|
||||||
|
<path d="M391.8 142.7c-5 21.7-.8 31.5 6.4 41 14.9 19.7 26.8 64.6 9.8 94"/>
|
||||||
|
<g stroke-linecap="round">
|
||||||
|
<path d="M417.5 252.3c2-.7 6-3.2 6.8-7.4m-5.2-2c.6-3.7 6.4-5.3 6.5-9.3m-6.4-5.2c-.4-3.9 5.8-7.4 4.9-11.2m-8.3-2.7c-.5-2.2 5.2-6.3 3.6-9.8m-7.8-3.8c-1.2-2.4 2.7-5.2 1-7.8m-7.2-3c-.4-1.6 2-5.3.7-7.5m-6.9-5.2c.5-.7 2.6-2.2 1.8-4.1m-6-5.3c.8-.4 3.3-1.2 3-3"/>
|
||||||
|
<path stroke-linejoin="round" d="M266 410.9c-5-1.8-11.5.7-12.8 5.1m3.9 4c.6-4.4 7.3-6.3 9.3-4.3-4.2-2.3-6.3 6-2.5 6.2m34.6-103.8c-3 1.6-4 7.2 0 11.5m4.6-10.2c-2.1 1.8-2 7.2 1.2 8.5-2.7-2 0-5.3 2-5.4 1.9-.1 3.2 2.2.8 4.5m177 5.2c-7.2-2-13 6.4-6.4 13.9-.2-7.2 5-12 11.3-10.7m-3 5a2.7 2.7 0 0 0-2.7 2.7c0 1.4 1.2 2.8 3.2 2.8 1.3 0 2.4-1.5 2.4-2.7m22.6-161c1.2 4.4 7.2 6.3 12 5.2m0-2.8c-3.7.1-6.8-3.4-6.6-6.3 0 2.2 5 3.2 6.6 1.8"/>
|
||||||
|
<path d="M267.9 331.7c-1-2.6 3-5.2 3.2-7.6.1-2.5 4.6-4.4 9.3.2m-2.2-27.4-2.5 1.2m11.4 12.8c-1.1 0-3.4 0-4.6-.9m0 5.8c-.8.5-2.8 1.2-4 1.5m-40.1 76.3c-.2 2.3 2.2 5.7 3.4 6.7m6.6-12.2a10 10 0 0 0-1.3 7.6m20.5 0c-2.3-1.5-.8-5.4-1-8-.3-2.6 2.7-6.7 8.5-3.2M246 381c2.3-.4 4.7-.3 6.3.4m23-7.7a8 8 0 0 0-1.4 4m12.3-4.3c-1.8 0-3.4 1.3-4.2 2.6m-20.8-68.5c2 .7 7.4 4 7.6 7.4m14.3-24.2c-6.3-.1-8.8-6.5-4-6.5m15.3 15.2c-2.4 1-1.3 5.2 2.2 7.3m-17.3 33.1c-1.2-1.6.4-6 4.4-4.7m5 51.7c.3-4 5.2-6.2 7.2-1.8m-25.5 13c-.3-4.3 2-5.7 3.8-6 2-.2 4.7 1.4 6 4.2m-48.1 5c.2-2.6 2.4-5.3 4.7-4.9m231-109.4c-1.7 1.2-2.8 6.7 3.5 7.2M458 296c0 .6.8 1.5 1.3 2m29 8.3c-1.6-1.3-6 4-2 7.7m-39 35.6c-.9-3.7 2.5-4.7 5.8-3.9m-14-22.2c2-1.2 4-2.7 6.4-3.3m-7.4 17.5c0-3 1.6-5.7 3-6.4m8.4-29.1a16 16 0 0 0 2.4 9.2m28.2-9q-3 .7-4.2 2.9m2.1 7.7q1.6-1.3 2.4-2.3m46.3-110.2c0 3.6-4.5 5.6-7.5 3.3m17.3-3.1c2 1.5 8.9 0 7.3-4M528 221.4a11 11 0 0 1-4.9-3m19.1-7.7c-2.3.5-3.9 0-5-.5m-31.6 13.4a17 17 0 0 0 6.5-1.6M502 200.8q-2.5-.3-3.9.8m29.8 5.5a11 11 0 0 1-3 5.2"/>
|
||||||
|
<path stroke-linejoin="round" d="M497.5 212.8c3.2-1.4 7.2 9.5 15 5.7m.6-11.4a11 11 0 0 0-1.8 5.6"/>
|
||||||
|
</g>
|
||||||
|
<path d="M359 190.4c1-.2 2.9-.5 3.3-1.8M226.5 310.3c3.9 2.2 6.6 5.9 5 11.4m172.1-143.2c1.2.8 5.5.8 8 0m3 2.6c0 1.8.5 8.6-3 10m1.3-.8c3.2 1 9.6.6 11.7-5.3m-4.5 5c1.7 2.6 2.2 7.6-2.8 10.3m4-6c3.8 1.2 12.4 1.4 11.5-6m-3 6c2.7 3.5 14 7.8 12.2.3m-22.5 10c4.3 1.1 10.5-1.9 8-9.6m12.3 3.9c.6 3 15 6 13.1-.7m-2.7 3.9c2.7 6.2 17 5.7 12.5-2.6m-2.3 6.5c2.8 3.4 15.5 1.4 10.4-7m-.1 6.8c7.9 6 17-2.5 7-8.7m4.6 6.8c7 5.5 15.5-4.5 9.4-7.3m-64.4 5c2.2.6 6.8.4 7.9-3.6m-1.8 2.7c-.2 5.8 9.6 8 12.1 1.3m-3.3 3.7c1.8 3.9 10.5 5.4 11.9-.1m-1.4 2.6c1.4 3.8 8.9 3.4 11-.6m-2.8 2.6c2.3 5 11.9 5 14-2.3m-1.6 3.2c3.7 2.3 11.7 1.4 11-5.8m-1.5 5.2c5.6 4.5 13.4.1 9.5-7.5m-.3 13.4c3-.5 4.5-6.4 1.4-8m-70 9c6-3.3 7.3-9 3-14.5m2.2 8.9c3.9 2.3 11.2-.2 12.5-5.8m-7.2 6.4c2.2 2.8 2.6 6.3-.3 9.5m2-5.9c6.9-4.2 15.4 3.6 9 8.4m-1-8.8c1.5-.4 4.2-3.2 4.4-6.4m-1.4 9.4c2.9-3 22.2 3 10 9.3m-1-17.4c3.8 1.4 5.7 6.7 0 8.5m5 4.4c4-4.3 17-1.6 12.3 3.8m-3.1-5.5c2.3-7.9 16.1-3 11.6.2m-14.4-8c.4 1.5.5 5.3-2 7.4m13.6-9a6 6 0 0 1-.8 5.3m9.5-5.2c.8 1.4 2 4.1-.8 6.3m-109.4-65.5c.1 7.3 2.7 12.2 12.6 7.6m-9.5 1.1c-5 6.6.6 13.7 10.3 6.6M365 165c6.7 7.2 18.7 2 11-9m8 15.2c-1.2 7.1 4.6 8.5 9.3 5.3m-34.2-10.3c1.2 7.1 8.5 12.7 15.6 8.4m-6.9 1.4c0 10.2 14 11.3 17.3.8m-5 6.4c4.5 9.3 14.3 5.5 17.5-.1m-27.4-14.7c1.8 4.5 5.4 9.5 13.7 5.8m-39.5-8c1.2 3.7 7.9 8.2 15.6 3m-10.8 1.8c-4.2 6 4 11.7 14 2.7m-9.1 4.7c1.6 8.5 5 15.4 17 4.4m-6 4.4c4.4 5.4 11.1 8.7 17.4-.4"/>
|
||||||
|
<path d="M387.3 188.8c-.4 6.5.8 9.7 5.9 9.4 4-.2 7.7-3.3 9.9-6.7m-10.8 6.7c-.2 7.4 5.6 13.2 16.5 5.7m-12 3c-2 5.5 4.1 14.5 16.2 9.9m-41.9-24.6c-.7 7 5.8 11.8 16.4 2.7m-11.2 5.1c.4 5.8 7 12.8 16.6 2.7m-12 4.7c-.7 9.9 8.4 12.7 16.1 5.1M367.2 200c2 .2 3.7-1.6 4.7-3m-.4 10.2c1.8.4 5-1.2 6-3.3m-1.4 15c2 2.3 9 .7 9.9-2.3m-2.2 2.2c3.8 9 14.1 8.7 18.4-1.5m-2 3.6c2 5.3 6.9 8.6 14.9 6.8m-11-1.4c-4.5 7.3 1.4 16 11.5 7.5m-9 3.5c-.5 4.6 3.7 9.9 9.5 10.5m-28.6-24c-1.5 10.4 6 15.4 15.3 9.8m-26.2-4.8c2.4 1.9 6.8 2.1 11 .6m5 6c-2.4 8.8 6.6 15.1 14.3 5.3M380 230.2c.2 4.5 4.5 9.4 12.1 8m21.3 9c-5.2 3.4-6.2 9.6 1 13.6m-13.6-15.4c.2 5.2 2.7 8.2 8.5 8.7m-16.1-11.4c-7.8 7-.2 15.3 9 8.3m-3.6 2.2c-2.6 8.1 7 13 12.2 4.8m-28.2-22c-2.8 8 .8 13.2 7.4 12.7m-17.6-14.3c.4 4.8 4.5 6.5 9.2 5.4m-6.2-.4c-3.5 6.7 1.8 10.3 8.8 7.8m21 15c-1 4.2-.4 7.2 5.9 8.7m-5.4-2.7c-7.6 3.4-8.3 10.8-2.4 15.5m-3.8-23.7a7.6 7.6 0 0 0 1.2 12.3m-35-35.8c-4.3 4.3-.2 16.2 9.5 9.7m15.7 5.3c-6 5-3.3 13.8 6.5 11.4m-16.3-15c-3.1 8.7-.2 11.8 6.8 11.9"/>
|
||||||
|
<path d="M359.3 236.1a8.2 8.2 0 0 0-1.5 12.2c2.3 2.6 6.7 1.4 8-1.9m-17.6-13.8c-5.9 7.6 0 16.6 8.4 14m23.3 8.6c-6 2.1-10.7 7.6-7 12.5 2.3 2.8 11.8 3.2 14.5-7.8M369 248.1c-3.5 5-2.4 9.8 4 12.2m-4.4-2.8c-3.8 2-6.2 4.5-5.3 9m-3.1-16.9c-1.4 6 .3 9.8 4 11.6m-4.1-4.4c-5.8-.4-8.8 2-6.8 7.8m.5-6.4c-5.7-2-6.6-7-4.1-12m-.8 7.6c-6.2.2-9 3.3-9 7.5"/>
|
||||||
|
<path d="M340.5 229.7c-4.5 1.9-6 8-4.3 11.3s7 3.6 10.2 1.4M328 224.9c-4.1 4.6.6 13.7 8 11.5m-18.6-15.1c-3.8 5.4.4 14.4 10.3 11.8m.5 25.1c-1-6.4 5.7-10.6 14-2.3m-4-13a10 10 0 0 0-3.3 8.5m-28-33.2c-4.3 7.2.9 13.8 10 11.2m13.8 6.7c-6 6-4 12.1.5 15.6m-4.4-8.5c-9.1.1-9.6 10.7-2.2 14m-4.7-24.6c-3.8 2.6-5 9.7 1.5 12.5m-1.8 3.4c-3.9-1.8-8.5.4-8.1 4.9m2.3-4.8c-3.5-8.2-13.6-6.8-12.7 1.7m15.5-11.9c-2.1.4-6.5 1.7-8.2 5m.6-14.3a7 7 0 0 0 2.5 11.4M296.7 216c-.8 5.6 1.4 8.3 8.4 7.8m-6-.7c-2.6 6.7 1 9.7 8 9.3m-6.4-1c-4.1 4.9-1.3 10.1 2.7 12.3m-4.6-6.1c-7.4-1-8.5 7.6-6 11.5"/>
|
||||||
|
<path d="M292.9 215.5c-4.5 2-7.1 7.6-4.7 11 2.5 3.5 7.4 2.4 10 .6m-8.4 1c-3.8 5.7-.4 10 3.6 11.6"/>
|
||||||
|
<path d="M275.3 214.8c-3 3.1-1.1 9.4 6 9.6 5.9 0 8.8-5.4 7-9.3m-8 9.3c-2.3 5.1-.8 10.7 8 9.8m3.6 8c-5.3-2.4-12.6 0-9.5 6.5m-.7-15.6c-2.2 3.1-1.5 7.5 1 10.2m-1.4-2c-3.6.7-7.1 2.6-5 8.3m-.5-4.6c-4.6-1.4-10.5 2-7 6.7m-.7-4.5c-4-.7-8.7 3.6-4.9 7.8m-1.5-3.5c-3.6 1-7.9 5-4.1 8.3m22.3-28.5c-5.2 2.3-5.7 8.1-3.3 12.4m-2.2-23c-7.7 3-7.6 13.2 1 16.4m-3.7-2.3c-4.6 2.7-5.5 7.9-2.4 11.2m-5.7-29.2c-3 1-2.8 10.5 5.4 10.3m-13.9-7.1c-6.2 4.1 1.3 14.5 11.3 7m-5.5 2.7c-.9 4.2.3 8.8 7.1 9.4m-6.7-3.8c-4.9 1.8-5.8 11.9 3 12.7m-18.6-21.8c-6.3 5.8 5.2 10.8 9.4 2.4m-18.1 7.2c-3.1 3.8 7.7 13.6 12.5-2.8m1.1-.3q.4 6 7.3 6.5m0 3.7c-7.7 1.2-10.1 10.7-1.6 12.8m-12.5-14c-.3 3.5 3.5 6.5 7.7 5.9m3.9 7.8c-7 1.6-7.9 10.6-1.7 10m-3.2 8.4c-5.8-1.7-6-8.6-.8-11.2"/>
|
||||||
|
<path d="M245.7 267.8c-4.9 3-3 10-.4 11s4.7-.2 5-2.4m-.5 10c.6 3.3-11.9 2-5.5-8.4m0 8.9c-4.2 6.6 2.6 12 6.9 6.3m-6.4 1.6c-1.7 5.3 4.7 9.1 8.8 5M231 245.4c-2.3 4.7 9.3 6.5 10-3.3m-13.2 10.3c-2.3 9.3 15.2 7.4 10.7-4.6m.4 6.2a8.2 8.2 0 0 0 11.6-6.3m-5.3 6.6a10 10 0 0 0 4.7 6.3m-13.1-3.9c-.8 5 4.3 9.2 10 8.8m-9.1-3.9c-2.8 3.9-3.7 11.3 5.2 11.8M224 263c-1.4 4 7 8.1 11.8 1.5m-14.4 8.7c-.7 3 6.8 7 11-5.8m-3.3 6.5c1.8 3.2 7.9 5.8 11.6-.5m-6 3.9c-1.6 4.5 2.7 8.7 7.8 7.5m-17.5-8.5c-1 6.4 6.5 10.1 11 6.8m-15.9-4c-2.2 8.2 8.4 11.3 12.4 5.1m-2.7 2.5c.4 4.6 7.6 8.6 13.2 4.8m-26.1-1c-.5 2.1 8.1 4.2 9.4-3m-4.4 4.9c2 5.2 9 6.5 13 0m-2.3 2.6c1 5 7.2 7.7 12.8 4.3m2.3 2c-1.4 6.5 5.4 11.8 9.6 8.2m-20.6-9.4c-2 7.2 7 11.3 12 7.3m-22.8-11c-.5 6.8 4.8 10.8 10.8 7.8m-22.3-7c-1.8 4.3 7.8 7.9 12.3 4.2m-18.2 7.7c2.5 2.8 11 0 11.7-6.3m-2.3 4.6c3.1 3.6 10.5 5.6 13.4-2.2m-2.6 4c0 5.6 9.8 9.6 13-.6m11.7 2c-1 2.7 1.2 7 5.7 7.5m-13.9-9.2c-.6 3 3.9 7.4 8.7 5.7m.9 1.3c-1.3 3.3-.2 8.1 4.3 8m-3.9-1.8c-3.4 2.8-2 7.9 2.9 8m-4.6-3.2c-4.8 3.2-3.1 10.3 3.2 9.9M239 313c0 7 8.7 8 10.3 1.6m-3.3 4c-1.3 4.2 2 8.2 7.3 7m-6.3 42.8c1.2 1.7 6-1.2 4.7-4s-6.4-1.3-5.8 1.7m4.6-2.9c.6-5.6-6-6.5-7.7-1.6m2.4-2.9c1.7-2.8-4-6.7-6.3-2.5m2-1.7c1.6-4.1-4.8-6-5.3-2.2m-1.5-5.4c.8-2 8-.8 5 3.5m5.5 5.1c2.8-2.6-2-7.8-5-6m24 13.9c-2.2.2-4.5 1.7-2.9 6.5 1.2 3.3 6.1 3.4 6.9 1.5"/>
|
||||||
|
<path d="M260.5 365.9c-2.2-1.5-7 1-4.8 5.6 1.6 3.5 5.8 2 6.4.2m-14 .8c1.2 2 6.6 1 7.6-1m-3.8-5.4c1.2-.8 3.3.2 3.7 1.2m-4-33c-3.8 2.5-1.5 10 4.3 8m-5.6-1.9c-3 2.3-.2 9.6 5.3 6.8m-4.3 0c-2.1 2.1-.1 7.8 5.2 6.7m-6-3.8c-1.2-.5-4.2-.2-5.5 1.8m2.1-28c-2.8 2.8-1.4 8.3 3.8 8.8m-4.8-3c-4 2-4 10 4 10.3m-4.3-1.3c-2.3 1.8-1.2 8.8 4.6 7.9m-2.8-.3c-.8.9-1 3-.2 3.9m-2.3-6.5c-2 0-4.8 1.5-5.5 3.7m-3.7-7.5c.7-2 7.1-2 7.6 4.1m.6-7.2c-.9.2-2.6 1-3 2.8m.2-21.7c-2.7 2.6-4 10.5 4.3 12m-9.9 4.4c0-2.1 5.8-4 7.8-.8M232 322.8c.8 1.6 4.8 3.8 7.2 2m-28-14.6a6.3 6.3 0 0 0 7.2 6.5c4.3-.5 5.2-4.2 3.9-6.7m-3.2 6.9c-2.9 3.4.6 8.3 3.8 6.9m-.6-9.4c1.7-.6 7.2-1.4 8.9 1.2m-26.8-.2c-1.3 2 6.7 3.9 9.4.2m-2.7 1.9c-.3 2.6 1 7 7.6 5.1"/>
|
||||||
|
<path d="M219.8 326.6c1-2.2-3.8-5.8-7.6-1.8s.6 8.5 2.9 7M202.7 318c-2 3.4 5.5 9.5 9.8 3.8"/>
|
||||||
|
<path d="M197.7 323.8c-2.7 2.8.7 7.9 4.7 6.3 4.1-1.5 3.7-5.8 2.7-6.9"/>
|
||||||
|
<path d="M192.5 329c-2.2 2 0 6.6 3 6.6s4.9-2.3 4.3-5.4m5-1.6c-.4 3 4 5 6.9 2.2m0-5.4c.5-.6.2-1.7-.5-2.3m-23.4 9.8c-2.5 2 3 7.6 6.3 2.5m13.1-3.5c-1.6 1.5 1 5.5 3.6 4.3m-12.4-1.5c.4 2.7 5.8 4.8 9.2.6m-6.3 2.1c-.8 1.8.9 4.5 3 4.2m48.3 11.8c-2 3.9 4.4 8.5 9 3.7m-4.4 2.1c-1 2.1.2 5.1 2 6.2m-8-1.7c.4-2 3.2-3.8 5.7-2.9m-9-2.2c.3-1.8 2.6-3.5 4.4-3m147.2-77.4c-9.2.1-5.3 14.8 2.6 11.9m-5.6-1c-1.8 2.9 1.7 7.6 5.6 4.8m-1 .6c-2.8 4.5 6.9 11.4 10.8 4.3m-3.1 2.7c0 4.3 12.5 7 10.7-1.5m-1.2 4.6c3 5 14 5.5 12.9-2m-2.5 5.2c2.3 3.4 13.2 5.4 12.8-1.5m-56.7-40.8c1.5 4 6.8 5.4 12.6 3m-16-.2c3.7 2.3-1.6 12.9-7.1 8.6m7.2-2c4 1.9 8.9.5 10.3-5.2m-2.5 4.4c.4 3 4.7 5.3 10.2 4.1m-20.1-1c5.2 4.4-2.3 13.4-5.7 9.3m7.6-6c2.5 1.8 8.8.8 9.4-4m-2.8 4c.5 3.1 3 4.3 6.4 4.4m-14.4-.1c2.6 3.6 9 4.6 11.9-.1m-2.9 2.7c-.2 3.4 3.6 6.3 7.6 5.5M375 295c3.3 1.7 7-4.7 4.1-9m-.2 15.7c4 .2 4.5-5.8.7-9.2m8.4 14.7c3.4-.8 2.3-8.1-5.8-8.7m16 14.1c3.2-.9.6-9-8.3-8.5m20.5 11.4c2.2-3.4-5-9-11-6m17 10.6c4 1.2 6.8-9.4-5.9-7.7m13.3 7.7c3.5-1 6-7.2-4.2-5.2m12 5.7c3 1.6 4.4-7.5-5-5m-47-23c3.9 2.1 10-.4 9.4-5.3m-1.5 4.2c1.3 2.3.8 6.8-1.7 8m2-2.6c2.6 1 6.1.1 8.2-4m-3.6 4c.7 1.8 1 5.4-.7 7.2m1.3-4.1c2.9 1.6 6.2-.5 7.6-3.5m-1.8 2.6c2 1.3 3.9 7.3.4 9.8m2-2.8c2.4 0 6 0 8.3-3m-1.7 1.7c2.2.6 4.8 4.2 3.9 7.4m.1-1.1c2.5-.3 6.7-2 7.9-5m-1.2 2a7 7 0 0 1 3.2 6.5m0-2.1c2.5-.1 4.7-1.4 5.3-4.1m-.6 1.8c1.8.6 4 2.7 4.1 5.1m-.3-1.5q2.6-.5 4.3-3.2m4.9-.3c2.3 2.3-.8 10-5.6 8.6m-43.9-164.7c-4.7 2.9-18.3 2-11.6-9.2m13 1.6c-9.5 3.8-21-3.5-9-11.3m-3.2-2.6c-7.8 0-13.1 12.6-2.6 17M348.6 138c-2 4 5.3 8.4 10.3 4.4 3.9-3.1 3.6-11.5 1.3-14.7m-15.6 19.8c-2.6 8.5 16.4 9 13.4-4.5m-14.4 17.2c.6 6.4 18.1 4.3 12.5-8.7m2-3.6c1.4 2 5.3 5.3 11.9 4.6m-9-13.5c.6 2.3 4.5 4 9.5 2.3M185.4 334.8c-4.5 3.1 2.4 8.1 4.9 2.6m-8.4-.2c-4.4 3.2 2.4 8.2 4.9 2.7m-8.6-.3c-4.4 3.2 2.4 8.1 5 2.7m-8.8-.5c-4.3 2.3.7 7.8 5 3m-9.2-.9c-4.3 2.3.7 7.8 5 3m16.6-9.6c-.1 3.3 6.1 4.6 8.4-.3m-4.5 3.3c-2 2.5 1.6 5.5 3.5 4.4m-11.8-4.8c-.2 2.8 5.2 4.4 7.8 1.3m-4.9 1.6c-1.7 2.2 1.7 5.3 4 4.4m-10.5-4.8c.2 2.2 3.3 4.7 6.6 3.3m-4.6-.3c-1.3 1.1-.7 3.8 1.1 4.7m-7.8-5c-.5 2 2.9 5.6 6.4 3.3m-5.7-1c-2 1.7-1.8 4.1.8 4.5m-5.8-4.7c-.2 1.4 1.5 3.6 3.7 3.3m-14.7-3.4c-2 1.1 3.2 6.7 6.5 1.5m-11.6.8c-2.6 1.2 3.8 7.4 6.7 1.1m10-1.3c-2 .4-3.3 3.3-1.6 4.5m-5-3.5c-.2 1.2 2.3 3 4.3 2.4m-5.9-1.9c-2 1.2-.9 4.6 1.4 4.1m-8.3-1.8c-1.8 1.3-.5 4 2 3.4m1.3-3.6c0 .7 1.7 1.8 3 1.3m-12.5-2.5c-2.3 1-2.1 6.6 5.5 3.6m-10.6-1.3c-3.2 1.5-2.1 5.8 4.8 1.7m.5.3c-.8.7-1.7 3.5 1.5 2.4m-7.2-1c-1.3 1.1-.5 3.5 2.3 2.6m-9-2.2c-2.3 1.3 2.8 3.1 6.2-.5m-4.6 2.1c-1 1-1 4.1 1.6 3m-8.2-1.7c-1.5 1.1 1 2.7 6 .5m-4 1.1c-1.9 1.5-1.7 3.4 1 2.8m-6.4-2c-2 1.9.9 3.1 4.4 1.6m-4.4.5c-3.4 1.5-1.9 4.4.2 3.3m113.8 6.5c-.8 1.7 1.8 3.1 4.3 2.7 2.3-.3 4.6-2.3 2.8-5m.1 3.4c2.4 1.8 6.5-.3 6.5-3"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#fff" stroke-width=".4">
|
||||||
|
<path d="M396.8 103c-10.3-5-31.7-14.6-37.8-6.9 5.6-2.3 21.8.1 35.2 12.5z"/>
|
||||||
|
<path d="M403 102.5c-11.9-13.9-19-10.8-27.5-15.5-8-4.5-20.8-5.4-23.3 1.7 11.7-5.7 22.5 3 29.2 4.2 9 1.4 14.2 8.5 16.9 11.7l4.7-2zm44.9-1.8c-6.2-14.1-19.4-10.4-25.2-16.4-8.5-8.8-30-17-39-10.9 19.6-1 28 13.5 38.4 18.6 7.5 3.6 15.5 11.3 25.8 8.7z"/>
|
||||||
|
<path d="M424.7 99.2c-10.5-13.1-26.8-24.7-34.2-20.4 9.8.6 12.9 7.4 19.8 11.8 7 4.3 3.9 10.7 14.4 8.6zm-50.2 23.2c-11-4.1-32.7-6.2-42.8 6.4 16.8 2.9 42 1.3 42.8-6.4z"/>
|
||||||
|
<path d="M372.4 127.3c-11-5.2-19.7 2-30.1 1-19.8-2-34-.8-35.8 8.8 11.3-10.2 30.4-1.8 38.1-3.9s36.3-.3 45.3 3.6c-4.6-5.7-11.8-7.2-17.5-9.5zm34.8-29.5c-2.6-8.5-2.4-17.5 10.3-16.9-3.2-4-15-6-17 8.9-14-10.3-29.4-12.1-32.2-3.2 7.2-6.2 18.4-1.7 31.8 13.5a24 24 0 0 1 7.1-2.3z"/>
|
||||||
|
<path d="M387.9 109.5c-8-5.2-18.8-13.5.1-16.9-8-4.3-20-2.4-18.7 12.5-21.6-8.7-37.1-5.8-40.4 2.9-3.6 9.5 9.8 14.8 12.1 8.7-2.4 1-10.8-1.8-6.4-7.2s26.8-1.3 48.1 9.8c6 3 26.3 2.6 5.2-9.8z"/>
|
||||||
|
<path d="M382.2 123.7c-6.1-12.6-26.1-1.2-30.1-13.4-5.6 17.9 28.4 8 30.1 13.4zm127.1 13.6c4.2 2.1 7.8-1.2 1.4-3.7 4.2 2 7.9-1.1 1.5-3.7 4.2 2 7.8-1.1 1.4-3.7-1.7 1.7-4.1 8-4.3 11.1zm2.2-24.2c9.3-9.8-.7-13.1 10.6-23.2 9.3-8.2 1.8-13.7 10.5-20 2.9-2.1 9-6.2 9.6-10.4 3.7 9.3-11.6 10.6-10.6 25.5.7 9.5-5.8 8.7-8.2 24.8-.5 3.3-2.9 10.8-11.9 3.3z"/>
|
||||||
|
<path d="M515.6 117.5c5.2-11 11.1-10.9 14-15.2 5.4-8.3 16.8 1.4 26.5-6-1.7 10.5-14.7 6.8-20.4 13.5s-10.3 9.7-20 7.7z"/>
|
||||||
|
<path d="M517 121.1c9-7.2 15.6-2.4 21.8-6.2 15.7-9.5 22 2 36-2.6-3.6 9-24.4 1.3-33.4 8s-40.7 13.2-24.5.8zm-26.3 51.4c-.2-4.1-4-9.4-9.4-10-5.4-.7-7.8-6.4-11.9-6.6-4.1-.3-6.8-8.5-12.5-8.4-5.6.1-8 7.5 5.3 14.2s28 14.4 28.5 10.8zm-16.8 3.5c-5.6.2-6.4 8.5-11.8 8.7 7.4 4 12.9-1.8 16.7-7L474 176z"/>
|
||||||
|
<path d="M478.6 177c-5 4.1-6.4 12.7.7 15.2-4.2-5.9 7.5-8.5 3.9-14z"/>
|
||||||
|
<path d="M483.4 177.3c-3.8 7.4 6.1 8.3 3.5 14 5.7-1.3 6.6-12 1.4-14.8l-4.9.7z"/>
|
||||||
|
<path d="M445.6 161.3c9.3-.5 17.7 4.5 23.4 12.6 3.6 5.1 15.7 7.2 19.9 3 4-4 1.8-12.8-8.5-10-2.6-4.3-10-2.8-13.7-6.4s-17.5-13.9-21.1.8z"/>
|
||||||
|
<path stroke-linecap="round" d="M480.4 167c-2.5.5-3.3 4.7-1.7 6.9m7-4.4c.7 1.3.2 3.1-.2 4m-25.2-11.8c4.7.2 5.6 3.7 10.8 5"/>
|
||||||
|
<path d="M457.1 150a161 161 0 0 1 37.6 12.3c8.1 4.6 20.7 6 31.6 2.8 11-3 32.2-5.9 31.1 7.8 5.8-6.9-1.5-14.2-16.2-15.5.3-6.7-6.9-12.7-12-8.7 4.7-.7 8.8 8.2-.6 11.8a8.4 8.4 0 0 0-11.7-9.8c4.4 1.3 9 8.8-1 11.6-6.3 1.8-15.4-.5-22.2-4.6s-45-19-36.6-7.8z"/>
|
||||||
|
<path d="M498.6 143.3c-5.1 2.3-1.8 7.7-9.7 10.8-8 3-13.6 10.1-11.8 16.2 5.4-11.9 15-11.3 18.3-16s8.3-11.4 3.2-11z"/>
|
||||||
|
<path d="M500 144c-.2 9.4-7.6 6-4.7 19.2.9 4.1 2.6 10.8-.3 17.5 8.3-6 3-18.7 6.7-23.5 1.8-2.4 4.2-6 5-9-1.9 5.3-1.7 15.4 3.5 18-4.2-10 11.5-18.3.7-30-1.6 3-6.5 8-10.8 7.8zm-26.2-9.4c1.3 2 2.6 6.8 1.4 9.6 2.6-1.6 6.3-5.6 7.5-8.3 5.3.8 7.3 7.3 2.3 10.2 3 0 8.4 0 11.3-3.5-3.6-4-14.8-10.2-22.5-8z"/>
|
||||||
|
<path d="M393.7 116.1a10 10 0 0 0-4.8-2c-7.8-1.6-3.7-8.6 2.9-8.5 14.2-15.5 22.1-3.6 39.4-8.4 6-1.8 10.2-1.3 13.4.2 7.8-5.2 16.8-3.8 23.4 2.3a5 5 0 0 1 3-2.4c6.1-1.7 11 3.4 12.7 10.1 4.7-.8 10.1 1.5 13.7 4.7q7.4-3.8 9.5 0c4.4-2 10-3.4 12.9 3.6s-6.7 4.9-8.3 19.6c-1 9-11 12.6-19 7.2-12.8-8.7-25.3-10-31.5 3-6.1 13.2-11 20.8-26 16.5a16 16 0 0 0-16.7 6.5c-4.4 6-11 .4-19 1 10-1.5 6.1-4 14.9-4.6 8-.5 5.9-8 11-9-20 5.1-19.3-2.4-35.8 2.8 7.2-9.3 18.6-4.1 24.2-9.5-14.9-.3-21.6-10-28.3-6-10.5 6.5-6 24.8-33.5 23.2-13.4-.7-21.9 1-29.8 9 13.9-28.8 32.8-13 42-22.1a68 68 0 0 0 12.3-14.7 6 6 0 0 1 4-3.2c-22.9-7.2-9-18.5 13.4-19.3z"/>
|
||||||
|
<path stroke-linecap="round" d="M506.9 112q.5 1.2.5 3c0 5.6-8.5 5.8-9 14.2-.3 4.4-.8 6.8-3.7 6.3s-5.6-5.1-2.8-10.5"/>
|
||||||
|
<path d="M398 107.7a11 11 0 0 0-6.2-2.1m52.8-8.3c6.8 3 10.1 10 20 10.4 9.6.3 15.5 14.2 31.5 5.1l1.2-.6M468 99.7a13 13 0 0 0-1.4 8.2m-29.8 21c-12 0-15.2 6.1-15.2 12s5.7 13.7 15.7 13.7 15.5-6.2 15.5-12.9-6.2-12.9-16-12.9z"/>
|
||||||
|
<path d="M439 154.3c-.2-3.3-6.3-2.8-6-5.4s3.3-3.6 3.3-7.2 5.4-3.9 7.2-1c1.8 2.8 7.2 8.3 8.4 5.5m-8.4-5.5a10 10 0 0 0-.4 13.1m4.3-8.8c-1 2-1.2 5-.1 6.8"/>
|
||||||
|
<path fill="none" d="M495.5 135.6c9.5.8 11-9.7 4-10.3m-15.7-17.8c-3.2-4.2-10.8-5.6-11 3.2"/>
|
||||||
|
<path d="M472 120.8c-3.2-8-11.5-9.1-15.8-5.1-3.7 3.3-3.8 12 4 13.8 2.8-3.3 8-7.5 11.8-8.7zm-3.5-5c-4.7-4.1-11.5 3.7-5.3 10.8m-83 8.7c3.2-.7 7.7.8 14.7 4 4.3 2.1 17.5 6.5 25.7 2.1-8.5 3.1-15-9.7-21.4-8.2-6.4 1.6-18.2 4-23.1-.8 12 .8 18.5-8.8 32.4-.5a24 24 0 0 0 13.1 3.6c-11.3-13.6-26.2-4.9-27.8-16 6.8 7.4 23.5-1.6 32 12.1m-29.5-10c-1.6-2-1-4-2.6-5.5"/>
|
||||||
|
</g>
|
||||||
|
<path stroke="none" d="M483.8 107.5c-2.8-3-9-.7-7.2 5.4a10 10 0 0 1 7.2-5.5zM466.3 124a21 21 0 0 1 5.7-3.2 12 12 0 0 0-3.8-5.2c-1.5-1-5.7 4.7-1.9 8.3z"/>
|
||||||
|
<path fill="none" stroke-linecap="round" stroke-width=".4" d="M458.7 113.5c-4.6-3.1-8.9-2.8-10.3-.3-3.3 0-6.1 2.4-6.2 7.2m7.1 2.1c-5.5-3.8-13.1-2.4-12.6 6.1m-3.4 4.9c2.7-2.2 6.6-3.8 9.6 0m31.8-8.4c-1 1.4-1.8 3.6-.1 7-2-2.8-7.2-2.8-12.8 4.4m25.1-11.8c-6.7.8-6.5 5.2-1.1 7m-46.5-23.4c-5.7-1.1-9.8 2.2-2 5m12.8-8.8c-8-2-11.2.1-7.6 2m-15.5 30c-.2 2.9 1.3 6.6 6 2.4m-4.4 6.1q.1 1.3-.7 2.4m-17.9-40c-4.9-1.5-5.8-6 0-5.6m-1.6 16c-5.4-1.9-5.4-7.1-1-6.6m11 3.8c-6.3-1.5-6.5-5.9-1.8-5.2m2.6-8.2c-3.4-.2-8.5 3.5.1 5.7m9.3 1.8c-7.9-1.1-7.7 2-3.1 4.1m9-11.6c-6-1.1-8.1 2.5-4 4m-15 18.3c-1.5-1.2-2.7-7 4.3-5.3m10.3 3.5c-4.7-1.3-9.2 3.4-4.9 6m11.2-11.5c-5-1-9.2.5-6.5 2.3"/>
|
||||||
|
<path fill="#fff" stroke-width=".4" d="M483.6 107.5a10 10 0 0 0-7 5.4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bv" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="bv-a">
|
||||||
|
<path fill-opacity=".7" d="M0 0h640v480H0z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g fill-rule="evenodd" stroke-width="1pt" clip-path="url(#bv-a)">
|
||||||
|
<path fill="#fff" d="M-28 0h699.7v512H-28z"/>
|
||||||
|
<path fill="#d72828" d="M-53-77.8h218.7v276.2H-53zM289.4-.6h381v199h-381zM-27.6 320h190.4v190.3H-27.6zm319.6 2.1h378.3v188.2H292z"/>
|
||||||
|
<path fill="#003897" d="M196.7-25.4H261v535.7h-64.5z"/>
|
||||||
|
<path fill="#003897" d="M-27.6 224.8h698v63.5h-698z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 582 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bw" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd">
|
||||||
|
<path fill="#00cbff" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fff" d="M0 160h640v160H0z"/>
|
||||||
|
<path fill="#000001" d="M0 186h640v108H0z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 264 B |