Релиз v1.0.0

This commit is contained in:
2026-08-09 12:06:46 +03:00
commit c3b3ce9901
14 changed files with 2207 additions and 0 deletions
+29
View File
@@ -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
+21
View File
@@ -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.
+18
View File
@@ -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
+6
View File
@@ -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
+33
View File
@@ -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")]
+53
View File
@@ -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>
+328
View File
@@ -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 являются отдельными проектами и распространяются на условиях собственных лицензий.
+2
View File
@@ -0,0 +1,2 @@
# Каждая ссылка Qobuz должна находиться на отдельной строке.
# Строки, начинающиеся с #, игнорируются.
View File
+40
View File
@@ -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
+510
View File
@@ -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
+93
View File
@@ -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
View File