Релиз v1.0.0
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
||||
# Visual Studio / .NET
|
||||
.vs/
|
||||
**/bin/
|
||||
**/obj/
|
||||
*.user
|
||||
*.suo
|
||||
*.pdb
|
||||
|
||||
# Загруженная музыка
|
||||
downloads/*
|
||||
!downloads/.gitkeep
|
||||
|
||||
# Локальные бинарные зависимости
|
||||
tools/*
|
||||
!tools/.gitkeep
|
||||
|
||||
# Состояние QobuzWatchdog
|
||||
data/completed_urls.txt
|
||||
data/failed_urls.txt
|
||||
|
||||
# Streamrip / временные файлы
|
||||
*.db
|
||||
*.tmp
|
||||
*.part
|
||||
*.log
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,18 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QobuzWatchdog", "QobuzWatchdog\QobuzWatchdog.csproj", "{6E3089C9-7E68-4F3F-B2BC-5DA23BDDED6B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6E3089C9-7E68-4F3F-B2BC-5DA23BDDED6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6E3089C9-7E68-4F3F-B2BC-5DA23BDDED6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6E3089C9-7E68-4F3F-B2BC-5DA23BDDED6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E3089C9-7E68-4F3F-B2BC-5DA23BDDED6B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||
</startup>
|
||||
</configuration>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// Общие сведения об этой сборке предоставляются следующим набором
|
||||
// набора атрибутов. Измените значения этих атрибутов для изменения сведений,
|
||||
// связанные с этой сборкой.
|
||||
[assembly: AssemblyTitle("QobuzWatchdog")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("QobuzWatchdog")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми
|
||||
// для компонентов COM. Если необходимо обратиться к типу в этой сборке через
|
||||
// из модели COM задайте для атрибута ComVisible этого типа значение true.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// Следующий GUID представляет идентификатор typelib, если этот проект доступен из модели COM
|
||||
[assembly: Guid("6e3089c9-7e68-4f3f-b2bc-5da23bdded6b")]
|
||||
|
||||
// Сведения о версии сборки состоят из указанных ниже четырех значений:
|
||||
//
|
||||
// Основной номер версии
|
||||
// Дополнительный номер версии
|
||||
// Номер сборки
|
||||
// Номер редакции
|
||||
//
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{6E3089C9-7E68-4F3F-B2BC-5DA23BDDED6B}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>QobuzWatchdog</RootNamespace>
|
||||
<AssemblyName>QobuzWatchdog</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,328 @@
|
||||
# Qobuz Streamrip Tools
|
||||
|
||||
Набор утилит для Windows для автоматизации работы со **Streamrip** и загрузками из **Qobuz**.
|
||||
|
||||
Проект объединяет:
|
||||
|
||||
- программу `QobuzWatchdog`, которая последовательно обрабатывает список ссылок через Streamrip и перезапускает зависший процесс;
|
||||
- очистку базы истории Streamrip;
|
||||
- приведение скачанных FLAC-файлов к CD-качеству;
|
||||
- группировку нескольких альбомов одного исполнителя в общую папку.
|
||||
|
||||
> Используйте проект только для контента, который вы имеете право скачивать и обрабатывать, и в соответствии с условиями используемых сервисов.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```text
|
||||
qobuz-streamrip-tools/
|
||||
├── QobuzWatchdog/
|
||||
│ ├── Properties/
|
||||
│ ├── App.config
|
||||
│ ├── Program.cs
|
||||
│ └── QobuzWatchdog.csproj
|
||||
│
|
||||
├── scripts/
|
||||
│ ├── Clear-StreamripDb.bat
|
||||
│ ├── Convert-FlacToCD.ps1
|
||||
│ └── GroupAlbumsByArtist.ps1
|
||||
│
|
||||
├── tools/
|
||||
│ └── .gitkeep
|
||||
│
|
||||
├── downloads/
|
||||
│ └── .gitkeep
|
||||
│
|
||||
├── data/
|
||||
│ └── urls.txt
|
||||
│
|
||||
├── QobuzWatchdog.sln
|
||||
├── README.md
|
||||
├── LICENSE
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
## Общий рабочий процесс
|
||||
|
||||
```text
|
||||
data/urls.txt
|
||||
↓
|
||||
QobuzWatchdog
|
||||
↓
|
||||
Streamrip
|
||||
↓
|
||||
downloads/
|
||||
↓
|
||||
Convert-FlacToCD.ps1
|
||||
↓
|
||||
GroupAlbumsByArtist.ps1
|
||||
```
|
||||
|
||||
Все загрузки Streamrip направляются в папку `downloads` в корне проекта.
|
||||
|
||||
`QobuzWatchdog` при каждом запуске проверяет `%APPDATA%\streamrip\config.toml` и устанавливает параметр `downloads.folder` в путь к локальной папке `downloads`.
|
||||
|
||||
## Скачать все зависимости:
|
||||
|
||||
[Скачать (.zip)](https://cloud.kolbox.duckdns.org/seafhttp/f/f918d0ca3aac4b4195de/?op=view)
|
||||
|
||||
## Основная зависимость — Streamrip
|
||||
|
||||
Streamrip:
|
||||
|
||||
https://github.com/nathom/streamrip
|
||||
|
||||
Для актуальной версии Streamrip необходим Python 3.10 или новее и `pip`.
|
||||
|
||||
Установка:
|
||||
|
||||
```powershell
|
||||
pip3 install streamrip --upgrade
|
||||
```
|
||||
|
||||
Проверка:
|
||||
|
||||
```powershell
|
||||
rip
|
||||
```
|
||||
|
||||
Перед первым использованием необходимо настроить Streamrip и авторизацию Qobuz.
|
||||
|
||||
Для Qobuz требуется подходящая подписка, позволяющая получать соответствующее качество аудио.
|
||||
|
||||
### Где ищется Streamrip
|
||||
|
||||
`QobuzWatchdog` ищет `rip.exe` в следующем порядке:
|
||||
|
||||
1. `tools\rip.exe`;
|
||||
2. системная переменная `PATH`.
|
||||
|
||||
Если Streamrip установлен через `pip`, обычно достаточно, чтобы каталог Python `Scripts` был добавлен в `PATH`.
|
||||
|
||||
## FFmpeg
|
||||
|
||||
Для `Convert-FlacToCD.ps1` необходимы:
|
||||
|
||||
```text
|
||||
ffmpeg.exe
|
||||
ffprobe.exe
|
||||
```
|
||||
|
||||
Сборка FFmpeg для Windows:
|
||||
|
||||
https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z
|
||||
|
||||
После распаковки файлы находятся в каталоге `bin`.
|
||||
|
||||
Их можно положить непосредственно в:
|
||||
|
||||
```text
|
||||
tools/
|
||||
```
|
||||
|
||||
Скрипт ищет `ffmpeg.exe` и `ffprobe.exe`:
|
||||
|
||||
1. в `tools`;
|
||||
2. затем в системном `PATH`.
|
||||
|
||||
## Зависимости по компонентам
|
||||
|
||||
| Компонент | Streamrip | FFmpeg | FFprobe | PowerShell | .NET Framework |
|
||||
| ------------------------- |:---------:|:------:|:-------:|:----------:|:--------------:|
|
||||
| `QobuzWatchdog` | ✓ | — | — | — | 4.7.2 |
|
||||
| `Clear-StreamripDb.bat` | ✓ | — | — | — | — |
|
||||
| `Convert-FlacToCD.ps1` | — | ✓ | ✓ | 5.1+ | — |
|
||||
| `GroupAlbumsByArtist.ps1` | — | — | — | 5.1+ | — |
|
||||
|
||||
## QobuzWatchdog
|
||||
|
||||
`QobuzWatchdog` читает ссылки из:
|
||||
|
||||
```text
|
||||
data/urls.txt
|
||||
```
|
||||
|
||||
Каждая ссылка должна находиться на отдельной строке.
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
https://www.qobuz.com/...
|
||||
https://open.qobuz.com/...
|
||||
```
|
||||
|
||||
Строки, начинающиеся с `#`, игнорируются.
|
||||
|
||||
Программа хранит состояние в:
|
||||
|
||||
```text
|
||||
data/completed_urls.txt
|
||||
data/failed_urls.txt
|
||||
```
|
||||
|
||||
Эти файлы создаются автоматически и не добавляются в Git.
|
||||
|
||||
### Что делает Watchdog
|
||||
|
||||
Для каждой ссылки программа:
|
||||
|
||||
1. запускает `rip --no-progress --verbose url "<URL>"`;
|
||||
2. отслеживает вывод процесса;
|
||||
3. если Streamrip долго не выводит ничего, завершает дерево процесса;
|
||||
4. повторяет попытку;
|
||||
5. сохраняет успешно обработанные ссылки;
|
||||
6. повторяет незавершённые ссылки в следующих проходах.
|
||||
|
||||
Текущие параметры находятся в `Program.cs`:
|
||||
|
||||
```csharp
|
||||
private const int MaxRunsPerUrl = 10;
|
||||
private const int MaxRounds = 20;
|
||||
private const int IdleLimitSeconds = 60;
|
||||
private const int RestartDelaySeconds = 3;
|
||||
```
|
||||
|
||||
### Сборка QobuzWatchdog
|
||||
|
||||
Проект рассчитан на:
|
||||
|
||||
```text
|
||||
.NET Framework 4.7.2
|
||||
```
|
||||
|
||||
Откройте:
|
||||
|
||||
```text
|
||||
QobuzWatchdog.sln
|
||||
```
|
||||
|
||||
в Visual Studio и соберите конфигурацию `Release`.
|
||||
|
||||
Каталоги `.vs`, `bin` и `obj` исключены из Git.
|
||||
|
||||
## Очистка базы Streamrip
|
||||
|
||||
Запустите:
|
||||
|
||||
```text
|
||||
scripts\Clear-StreamripDb.bat
|
||||
```
|
||||
|
||||
Скрипт удаляет:
|
||||
|
||||
```text
|
||||
%APPDATA%\streamrip\downloads.db
|
||||
%APPDATA%\streamrip\failed_downloads.db
|
||||
```
|
||||
|
||||
Это сбрасывает историю скачанных и неудачно обработанных элементов Streamrip.
|
||||
|
||||
## Конвертация FLAC в CD-качество
|
||||
|
||||
Запустите:
|
||||
|
||||
```powershell
|
||||
.\scripts\Convert-FlacToCD.ps1
|
||||
```
|
||||
|
||||
Скрипт автоматически обрабатывает:
|
||||
|
||||
```text
|
||||
downloads/
|
||||
```
|
||||
|
||||
Целевые параметры:
|
||||
|
||||
```text
|
||||
FLAC
|
||||
16 бит
|
||||
44,1 кГц
|
||||
2 канала
|
||||
```
|
||||
|
||||
Скрипт:
|
||||
|
||||
- рекурсивно ищет FLAC;
|
||||
- пропускает уже подходящие файлы;
|
||||
- сохраняет метаданные и встроенные изображения;
|
||||
- создаёт временный файл;
|
||||
- проверяет результат через FFprobe;
|
||||
- только после успешной проверки заменяет исходник;
|
||||
- при ошибке пытается восстановить исходный файл.
|
||||
|
||||
## Группировка альбомов по исполнителям
|
||||
|
||||
Запустите:
|
||||
|
||||
```powershell
|
||||
.\scripts\GroupAlbumsByArtist.ps1
|
||||
```
|
||||
|
||||
Скрипт работает с папкой `downloads`.
|
||||
|
||||
Он ожидает названия альбомов в формате:
|
||||
|
||||
```text
|
||||
Исполнитель - Альбом
|
||||
```
|
||||
|
||||
Если у одного исполнителя найдено два или больше альбомов:
|
||||
|
||||
```text
|
||||
downloads/
|
||||
├── Исполнитель/
|
||||
│ ├── Исполнитель - Альбом 1/
|
||||
│ └── Исполнитель - Альбом 2/
|
||||
```
|
||||
|
||||
Если альбом только один, его папка остаётся на первом уровне.
|
||||
|
||||
## Папка downloads
|
||||
|
||||
Вся музыка находится в:
|
||||
|
||||
```text
|
||||
downloads/
|
||||
```
|
||||
|
||||
Содержимое этой папки исключено из Git:
|
||||
|
||||
```gitignore
|
||||
downloads/*
|
||||
!downloads/.gitkeep
|
||||
```
|
||||
|
||||
## Папка tools
|
||||
|
||||
В `tools` можно положить локальные зависимости:
|
||||
|
||||
```text
|
||||
rip.exe
|
||||
ffmpeg.exe
|
||||
ffprobe.exe
|
||||
```
|
||||
|
||||
Бинарные файлы из `tools` не добавляются в Git.
|
||||
|
||||
## PowerShell
|
||||
|
||||
Скрипты рассчитаны на Windows PowerShell 5.1 или новее.
|
||||
|
||||
Если Windows блокирует запуск локальных `.ps1`, проверьте текущую политику:
|
||||
|
||||
```powershell
|
||||
Get-ExecutionPolicy
|
||||
```
|
||||
|
||||
При необходимости для текущего пользователя можно использовать:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
||||
```
|
||||
|
||||
Меняйте политику выполнения только если понимаете последствия этой настройки.
|
||||
|
||||
## Лицензия
|
||||
|
||||
Исходный код этого репозитория распространяется по лицензии MIT.
|
||||
|
||||
Streamrip и FFmpeg являются отдельными проектами и распространяются на условиях собственных лицензий.
|
||||
@@ -0,0 +1,2 @@
|
||||
# Каждая ссылка Qobuz должна находиться на отдельной строке.
|
||||
# Строки, начинающиеся с #, игнорируются.
|
||||
@@ -0,0 +1,40 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal
|
||||
|
||||
set "STREAMRIP_DIR=%APPDATA%\streamrip"
|
||||
set "DOWNLOADS_DB=%STREAMRIP_DIR%\downloads.db"
|
||||
set "FAILED_DB=%STREAMRIP_DIR%\failed_downloads.db"
|
||||
|
||||
echo.
|
||||
echo ============================================
|
||||
echo ОЧИСТКА БАЗЫ STREAMRIP
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
if exist "%DOWNLOADS_DB%" (
|
||||
del /q "%DOWNLOADS_DB%" >nul 2>&1
|
||||
if exist "%DOWNLOADS_DB%" (
|
||||
echo ОШИБКА: не удалось удалить downloads.db
|
||||
) else (
|
||||
echo downloads.db удалён.
|
||||
)
|
||||
) else (
|
||||
echo downloads.db не найден — пропущено.
|
||||
)
|
||||
|
||||
if exist "%FAILED_DB%" (
|
||||
del /q "%FAILED_DB%" >nul 2>&1
|
||||
if exist "%FAILED_DB%" (
|
||||
echo ОШИБКА: не удалось удалить failed_downloads.db
|
||||
) else (
|
||||
echo failed_downloads.db удалён.
|
||||
)
|
||||
) else (
|
||||
echo failed_downloads.db не найден — пропущено.
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Готово.
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,510 @@
|
||||
#requires -Version 5.1
|
||||
|
||||
<#
|
||||
Конвертация всех FLAC-файлов в CD-качество:
|
||||
16 бит / 44,1 кГц / стерео.
|
||||
|
||||
Скрипт:
|
||||
- рекурсивно ищет все *.flac;
|
||||
- пропускает файлы, которые уже 16 бит / 44100 Гц / стерео;
|
||||
- сохраняет теги и встроенную обложку;
|
||||
- проверяет результат через ffprobe;
|
||||
- после успешной проверки заменяет исходный файл;
|
||||
- показывает прогресс;
|
||||
- в конце выводит список файлов с ошибками.
|
||||
#>
|
||||
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$ToolsFolder = Join-Path $ProjectRoot "tools"
|
||||
$RootFolder = Join-Path $ProjectRoot "downloads"
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Find-Tool {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CommandName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$LocalNames
|
||||
)
|
||||
|
||||
foreach ($LocalName in $LocalNames) {
|
||||
$LocalPath = Join-Path $ToolsFolder $LocalName
|
||||
if (Test-Path -LiteralPath $LocalPath -PathType Leaf) {
|
||||
return (Resolve-Path -LiteralPath $LocalPath).Path
|
||||
}
|
||||
}
|
||||
|
||||
$Command = Get-Command $CommandName -ErrorAction SilentlyContinue
|
||||
if ($Command) {
|
||||
return $Command.Source
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
$FfmpegPath = Find-Tool -CommandName "ffmpeg.exe" -LocalNames @("ffmpeg.exe")
|
||||
$FfprobePath = Find-Tool -CommandName "ffprobe.exe" -LocalNames @("ffprobe.exe")
|
||||
|
||||
|
||||
try {
|
||||
$Host.UI.RawUI.WindowTitle = "FLAC -> CD quality"
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
function Wait-BeforeExit {
|
||||
Write-Host ""
|
||||
[void](Read-Host "Нажмите Enter для выхода")
|
||||
}
|
||||
|
||||
function Get-FlacAudioInfo {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$probeOutput = & $FfprobePath `
|
||||
-v error `
|
||||
-select_streams a:0 `
|
||||
-show_entries "stream=codec_name,sample_rate,channels,bits_per_sample,bits_per_raw_sample,sample_fmt" `
|
||||
-of json `
|
||||
-- `
|
||||
$Path 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffprobe не смог прочитать файл:`n$probeOutput"
|
||||
}
|
||||
|
||||
$probeData = $probeOutput | ConvertFrom-Json
|
||||
|
||||
if (
|
||||
-not $probeData.streams -or
|
||||
$probeData.streams.Count -lt 1
|
||||
) {
|
||||
throw "Аудиопоток не найден."
|
||||
}
|
||||
|
||||
$stream = $probeData.streams[0]
|
||||
$bitDepth = 0
|
||||
|
||||
if (
|
||||
$null -ne $stream.bits_per_raw_sample -and
|
||||
[string]$stream.bits_per_raw_sample -ne "" -and
|
||||
[int]$stream.bits_per_raw_sample -gt 0
|
||||
) {
|
||||
$bitDepth = [int]$stream.bits_per_raw_sample
|
||||
}
|
||||
elseif (
|
||||
$null -ne $stream.bits_per_sample -and
|
||||
[string]$stream.bits_per_sample -ne "" -and
|
||||
[int]$stream.bits_per_sample -gt 0
|
||||
) {
|
||||
$bitDepth = [int]$stream.bits_per_sample
|
||||
}
|
||||
elseif ([string]$stream.sample_fmt -match "s16") {
|
||||
$bitDepth = 16
|
||||
}
|
||||
elseif ([string]$stream.sample_fmt -match "s32") {
|
||||
$bitDepth = 24
|
||||
}
|
||||
|
||||
[PSCustomObject]@{
|
||||
Codec = [string]$stream.codec_name
|
||||
SampleRate = [int]$stream.sample_rate
|
||||
Channels = [int]$stream.channels
|
||||
BitDepth = [int]$bitDepth
|
||||
SampleFmt = [string]$stream.sample_fmt
|
||||
}
|
||||
}
|
||||
|
||||
Clear-Host
|
||||
|
||||
Write-Host "FLAC -> CD quality" -ForegroundColor Cyan
|
||||
Write-Host "16 бит / 44,1 кГц / стерео"
|
||||
Write-Host "Папка: $RootFolder"
|
||||
Write-Host ""
|
||||
|
||||
if (
|
||||
-not (
|
||||
Test-Path `
|
||||
-LiteralPath $RootFolder `
|
||||
-PathType Container
|
||||
)
|
||||
) {
|
||||
Write-Host "ОШИБКА: папка не найдена:" -ForegroundColor Red
|
||||
Write-Host $RootFolder -ForegroundColor Red
|
||||
|
||||
Wait-BeforeExit
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $FfmpegPath) {
|
||||
Write-Host "ОШИБКА: ffmpeg.exe не найден." -ForegroundColor Red
|
||||
Write-Host "Проверены папка tools и системный PATH."
|
||||
Wait-BeforeExit
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $FfprobePath) {
|
||||
Write-Host "ОШИБКА: ffprobe.exe не найден." -ForegroundColor Red
|
||||
Write-Host "Проверены папка tools и системный PATH."
|
||||
Wait-BeforeExit
|
||||
exit 1
|
||||
}
|
||||
|
||||
$files = @(
|
||||
Get-ChildItem `
|
||||
-LiteralPath $RootFolder `
|
||||
-Filter "*.flac" `
|
||||
-File `
|
||||
-Recurse |
|
||||
Where-Object {
|
||||
$_.Name -notlike "*.__cd_temp__.flac"
|
||||
} |
|
||||
Sort-Object FullName
|
||||
)
|
||||
|
||||
$total = $files.Count
|
||||
|
||||
if ($total -eq 0) {
|
||||
Write-Host "FLAC-файлы не найдены." `
|
||||
-ForegroundColor Yellow
|
||||
|
||||
Wait-BeforeExit
|
||||
exit 0
|
||||
}
|
||||
|
||||
$converted = 0
|
||||
$skipped = 0
|
||||
$failed = 0
|
||||
$index = 0
|
||||
$startTime = Get-Date
|
||||
|
||||
$failedFiles = [System.Collections.Generic.List[object]]::new()
|
||||
|
||||
foreach ($file in $files) {
|
||||
$index++
|
||||
|
||||
$percent = [int][Math]::Floor(
|
||||
(($index - 1) / $total) * 100
|
||||
)
|
||||
|
||||
Write-Progress `
|
||||
-Activity "Конвертация FLAC в CD-качество" `
|
||||
-Status (
|
||||
"Файл {0} из {1}: {2}" -f `
|
||||
$index,
|
||||
$total,
|
||||
$file.Name
|
||||
) `
|
||||
-CurrentOperation $file.DirectoryName `
|
||||
-PercentComplete $percent
|
||||
|
||||
Write-Host ""
|
||||
|
||||
Write-Host (
|
||||
"[{0}/{1}] {2}" -f `
|
||||
$index,
|
||||
$total,
|
||||
$file.FullName
|
||||
) -ForegroundColor White
|
||||
|
||||
$tempFile = Join-Path `
|
||||
-Path $file.DirectoryName `
|
||||
-ChildPath (
|
||||
$file.BaseName +
|
||||
".__cd_temp__.flac"
|
||||
)
|
||||
|
||||
$backupFile = Join-Path `
|
||||
-Path $file.DirectoryName `
|
||||
-ChildPath (
|
||||
$file.Name +
|
||||
".__original_backup__"
|
||||
)
|
||||
|
||||
try {
|
||||
if (Test-Path -LiteralPath $tempFile) {
|
||||
Remove-Item `
|
||||
-LiteralPath $tempFile `
|
||||
-Force
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $backupFile) {
|
||||
Remove-Item `
|
||||
-LiteralPath $backupFile `
|
||||
-Force
|
||||
}
|
||||
|
||||
$sourceInfo = Get-FlacAudioInfo `
|
||||
-Path $file.FullName
|
||||
|
||||
Write-Host (
|
||||
"Исходник: {0} бит, {1} Гц, {2} канал(а)" -f `
|
||||
$sourceInfo.BitDepth,
|
||||
$sourceInfo.SampleRate,
|
||||
$sourceInfo.Channels
|
||||
)
|
||||
|
||||
$alreadyCdQuality = (
|
||||
$sourceInfo.BitDepth -eq 16 -and
|
||||
$sourceInfo.SampleRate -eq 44100 -and
|
||||
$sourceInfo.Channels -eq 2
|
||||
)
|
||||
|
||||
if ($alreadyCdQuality) {
|
||||
$skipped++
|
||||
|
||||
Write-Host `
|
||||
"Уже CD-качество — пропущено." `
|
||||
-ForegroundColor Green
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Конвертация..." `
|
||||
-ForegroundColor Yellow
|
||||
|
||||
$ffmpegOutput = & $FfmpegPath `
|
||||
-hide_banner `
|
||||
-loglevel error `
|
||||
-y `
|
||||
-i `
|
||||
$file.FullName `
|
||||
-map 0:a:0 `
|
||||
-map "0:v?" `
|
||||
-map_metadata 0 `
|
||||
-c:a flac `
|
||||
-sample_fmt s16 `
|
||||
-ar 44100 `
|
||||
-ac 2 `
|
||||
-compression_level 8 `
|
||||
-c:v copy `
|
||||
-- `
|
||||
$tempFile 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffmpeg завершился с ошибкой:`n$ffmpegOutput"
|
||||
}
|
||||
|
||||
if (
|
||||
-not (
|
||||
Test-Path `
|
||||
-LiteralPath $tempFile `
|
||||
-PathType Leaf
|
||||
)
|
||||
) {
|
||||
throw "Временный файл не был создан."
|
||||
}
|
||||
|
||||
$tempFileInfo = Get-Item `
|
||||
-LiteralPath $tempFile
|
||||
|
||||
if ($tempFileInfo.Length -le 0) {
|
||||
throw "Создан пустой временный файл."
|
||||
}
|
||||
|
||||
$resultInfo = Get-FlacAudioInfo `
|
||||
-Path $tempFile
|
||||
|
||||
$validResult = (
|
||||
$resultInfo.BitDepth -eq 16 -and
|
||||
$resultInfo.SampleRate -eq 44100 -and
|
||||
$resultInfo.Channels -eq 2
|
||||
)
|
||||
|
||||
if (-not $validResult) {
|
||||
throw (
|
||||
"Проверка результата не пройдена. " +
|
||||
"Получено: {0} бит, {1} Гц, " +
|
||||
"{2} канал(а)." -f `
|
||||
$resultInfo.BitDepth,
|
||||
$resultInfo.SampleRate,
|
||||
$resultInfo.Channels
|
||||
)
|
||||
}
|
||||
|
||||
$originalCreationTime = $file.CreationTime
|
||||
$originalLastWriteTime = $file.LastWriteTime
|
||||
|
||||
Move-Item `
|
||||
-LiteralPath $file.FullName `
|
||||
-Destination $backupFile
|
||||
|
||||
try {
|
||||
Move-Item `
|
||||
-LiteralPath $tempFile `
|
||||
-Destination $file.FullName
|
||||
|
||||
$newFile = Get-Item `
|
||||
-LiteralPath $file.FullName
|
||||
|
||||
$newFile.CreationTime = $originalCreationTime
|
||||
$newFile.LastWriteTime = $originalLastWriteTime
|
||||
|
||||
Remove-Item `
|
||||
-LiteralPath $backupFile `
|
||||
-Force
|
||||
}
|
||||
catch {
|
||||
if (
|
||||
Test-Path `
|
||||
-LiteralPath $file.FullName
|
||||
) {
|
||||
Remove-Item `
|
||||
-LiteralPath $file.FullName `
|
||||
-Force `
|
||||
-ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (
|
||||
Test-Path `
|
||||
-LiteralPath $backupFile
|
||||
) {
|
||||
Move-Item `
|
||||
-LiteralPath $backupFile `
|
||||
-Destination $file.FullName `
|
||||
-ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
throw
|
||||
}
|
||||
|
||||
$converted++
|
||||
|
||||
Write-Host `
|
||||
"Готово — исходный файл заменён." `
|
||||
-ForegroundColor Cyan
|
||||
}
|
||||
catch {
|
||||
$failed++
|
||||
|
||||
$errorMessage = $_.Exception.Message
|
||||
|
||||
$failedFiles.Add(
|
||||
[PSCustomObject]@{
|
||||
Name = $file.BaseName
|
||||
FileName = $file.Name
|
||||
FullName = $file.FullName
|
||||
Directory = $file.DirectoryName
|
||||
Error = $errorMessage
|
||||
}
|
||||
)
|
||||
|
||||
Write-Host "ОШИБКА:" `
|
||||
-ForegroundColor Red
|
||||
|
||||
Write-Host $errorMessage `
|
||||
-ForegroundColor Red
|
||||
|
||||
if (
|
||||
Test-Path `
|
||||
-LiteralPath $tempFile
|
||||
) {
|
||||
Remove-Item `
|
||||
-LiteralPath $tempFile `
|
||||
-Force `
|
||||
-ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (
|
||||
(Test-Path -LiteralPath $backupFile) -and
|
||||
-not (
|
||||
Test-Path `
|
||||
-LiteralPath $file.FullName
|
||||
)
|
||||
) {
|
||||
Move-Item `
|
||||
-LiteralPath $backupFile `
|
||||
-Destination $file.FullName `
|
||||
-ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Progress `
|
||||
-Activity "Конвертация FLAC в CD-качество" `
|
||||
-Completed
|
||||
|
||||
$elapsed = (Get-Date) - $startTime
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" `
|
||||
-ForegroundColor DarkGray
|
||||
|
||||
Write-Host "Обработка завершена" `
|
||||
-ForegroundColor Cyan
|
||||
|
||||
Write-Host (
|
||||
"Всего файлов: {0}" -f $total
|
||||
)
|
||||
|
||||
Write-Host (
|
||||
"Конвертировано: {0}" -f $converted
|
||||
) -ForegroundColor Cyan
|
||||
|
||||
Write-Host (
|
||||
"Пропущено (уже CD): {0}" -f $skipped
|
||||
) -ForegroundColor Green
|
||||
|
||||
if ($failed -gt 0) {
|
||||
Write-Host (
|
||||
"Ошибок: {0}" -f $failed
|
||||
) -ForegroundColor Red
|
||||
}
|
||||
else {
|
||||
Write-Host "Ошибок: 0" `
|
||||
-ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host (
|
||||
"Время: {0:hh\:mm\:ss}" -f $elapsed
|
||||
)
|
||||
|
||||
Write-Host "========================================" `
|
||||
-ForegroundColor DarkGray
|
||||
|
||||
if ($failedFiles.Count -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "ТРЕКИ С ОШИБКАМИ:" `
|
||||
-ForegroundColor Red
|
||||
|
||||
Write-Host "========================================" `
|
||||
-ForegroundColor DarkGray
|
||||
|
||||
$failedNumber = 0
|
||||
|
||||
foreach ($failedFile in $failedFiles) {
|
||||
$failedNumber++
|
||||
|
||||
Write-Host (
|
||||
"{0}. {1}" -f `
|
||||
$failedNumber,
|
||||
$failedFile.Name
|
||||
) -ForegroundColor Red
|
||||
|
||||
Write-Host (
|
||||
" Файл: {0}" -f `
|
||||
$failedFile.FullName
|
||||
) -ForegroundColor DarkGray
|
||||
|
||||
Write-Host (
|
||||
" Ошибка: {0}" -f `
|
||||
$failedFile.Error
|
||||
) -ForegroundColor DarkYellow
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Write-Host "========================================" `
|
||||
-ForegroundColor DarkGray
|
||||
}
|
||||
else {
|
||||
Write-Host ""
|
||||
Write-Host "Все файлы обработаны без ошибок." `
|
||||
-ForegroundColor Green
|
||||
}
|
||||
|
||||
Wait-BeforeExit
|
||||
@@ -0,0 +1,93 @@
|
||||
#requires -Version 5.1
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$RootPath = Join-Path $ProjectRoot "downloads"
|
||||
$Separator = " - "
|
||||
|
||||
try {
|
||||
$Host.UI.RawUI.WindowTitle = "Группировка альбомов по исполнителям"
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
function Wait-BeforeExit {
|
||||
Write-Host ""
|
||||
[void](Read-Host "Нажмите Enter для выхода")
|
||||
}
|
||||
|
||||
Clear-Host
|
||||
|
||||
Write-Host "Группировка альбомов по исполнителям" -ForegroundColor Cyan
|
||||
Write-Host "Папка: $RootPath"
|
||||
Write-Host ""
|
||||
|
||||
if (-not (Test-Path -LiteralPath $RootPath -PathType Container)) {
|
||||
New-Item -ItemType Directory -Path $RootPath -Force | Out-Null
|
||||
}
|
||||
|
||||
$AlbumFolders = Get-ChildItem -LiteralPath $RootPath -Directory |
|
||||
Where-Object {
|
||||
$_.Name.Contains($Separator)
|
||||
} |
|
||||
ForEach-Object {
|
||||
$Artist = ($_.Name -split [regex]::Escape($Separator), 2)[0].Trim()
|
||||
|
||||
[PSCustomObject]@{
|
||||
Artist = $Artist
|
||||
Folder = $_
|
||||
}
|
||||
}
|
||||
|
||||
$ArtistGroups = $AlbumFolders | Group-Object -Property Artist
|
||||
|
||||
$Moved = 0
|
||||
$Skipped = 0
|
||||
|
||||
foreach ($Group in $ArtistGroups) {
|
||||
# Папка исполнителя создаётся только при наличии двух и более альбомов.
|
||||
if ($Group.Count -lt 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
$ArtistName = $Group.Name
|
||||
$ArtistPath = Join-Path -Path $RootPath -ChildPath $ArtistName
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ArtistPath -PathType Container)) {
|
||||
New-Item -Path $ArtistPath -ItemType Directory | Out-Null
|
||||
Write-Host "Создана папка: $ArtistName" -ForegroundColor Green
|
||||
}
|
||||
|
||||
foreach ($Album in $Group.Group) {
|
||||
$SourcePath = $Album.Folder.FullName
|
||||
$DestinationPath = Join-Path -Path $ArtistPath -ChildPath $Album.Folder.Name
|
||||
|
||||
if (Test-Path -LiteralPath $DestinationPath) {
|
||||
Write-Warning "Папка уже существует, пропущено: $DestinationPath"
|
||||
$Skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
Move-Item `
|
||||
-LiteralPath $SourcePath `
|
||||
-Destination $ArtistPath `
|
||||
-ErrorAction Stop
|
||||
|
||||
Write-Host "Перемещено: $($Album.Folder.Name) -> $ArtistName" -ForegroundColor Cyan
|
||||
$Moved++
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Не удалось переместить '$SourcePath': $($_.Exception.Message)"
|
||||
$Skipped++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Обработка завершена." -ForegroundColor Yellow
|
||||
Write-Host "Перемещено: $Moved"
|
||||
Write-Host "Пропущено: $Skipped"
|
||||
|
||||
Wait-BeforeExit
|
||||
Reference in New Issue
Block a user