Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #############################################################
- # Requirements: FFmpeg for audiobooks and Calibre for eBooks
- # Run this command in Powershell to execute the script:
- # -ExecutionPolicy Bypass -File .\thisFile.ps1
- #############################################################
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
- Add-Type -AssemblyName System.IO.Compression.FileSystem
- $ROOT = Get-Location
- Get-ChildItem -Path $ROOT -Directory -Recurse |
- Where-Object { $_.Name -eq "!Side Stories" } |
- ForEach-Object {
- $parentName = $_.Parent.Name
- $newName = "$parentName - Side stories"
- Rename-Item -Path $_.FullName -NewName $newName
- }
- $FFPROBE = "ffprobe.exe"
- # if ffprobe.exe is not in Windows PATH, use
- # $FFPROBE="location/of/ffprobe.exe"
- function Sanitize($text) {
- if ([string]::IsNullOrWhiteSpace($text)) { return $text }
- $text = $text -replace '[<>:"/\\|?*]', ' '
- $text = $text.Trim().TrimEnd('.')
- return $text
- }
- function NormalizeAuthor($author) {
- if ($author -match "(.+),\s*(.+)") {
- return "$($Matches[2]) $($Matches[1])"
- }
- return $author
- }
- function Get-EpubMetadata($file) {
- $AUTHOR = ""
- $TITLE = ""
- $SERIES = ""
- $INDEX = ""
- try {
- $zip = [System.IO.Compression.ZipFile]::OpenRead($file)
- # container.xml tells us where the OPF is
- $containerEntry = $zip.GetEntry("META-INF/container.xml")
- $reader = New-Object IO.StreamReader($containerEntry.Open())
- $containerXml = [xml]$reader.ReadToEnd()
- $reader.Close()
- $opfPath = $containerXml.container.rootfiles.rootfile.'full-path'
- $opfEntry = $zip.GetEntry($opfPath)
- $reader = New-Object IO.StreamReader($opfEntry.Open())
- $opfXml = [xml]$reader.ReadToEnd()
- $reader.Close()
- $zip.Dispose()
- # Set up namespace manager for dc: and opf: namespaces
- $nsmgr = New-Object System.Xml.XmlNamespaceManager($opfXml.NameTable)
- $nsmgr.AddNamespace("dc","http://purl.org/dc/elements/1.1/")
- $nsmgr.AddNamespace("opf","http://www.idpf.org/2007/opf")
- $metadataNode = $opfXml.SelectSingleNode("//opf:metadata", $nsmgr)
- # Title
- $titleNode = $metadataNode.SelectSingleNode("dc:title", $nsmgr)
- if ($titleNode) { $TITLE = $titleNode.InnerText }
- # Authors (handle multiple)
- $authorNodes = $metadataNode.SelectNodes("dc:creator", $nsmgr)
- if ($authorNodes) {
- $AUTHOR = ($authorNodes | ForEach-Object { $_.InnerText }) -join ", "
- }
- # Series and Index from Calibre meta tags
- $metaNodes = $metadataNode.SelectNodes("opf:meta", $nsmgr)
- foreach ($m in $metaNodes) {
- if ($m.GetAttribute("name") -eq "calibre:series") { $SERIES = $m.GetAttribute("content") }
- if ($m.GetAttribute("name") -eq "calibre:series_index") { $INDEX = $m.GetAttribute("content") }
- }
- } catch {
- # Fail silently
- }
- return @{
- Author = $AUTHOR
- Title = $TITLE
- Series = $SERIES
- Index = $INDEX
- }
- }
- function Get-M4BMetadata($file) {
- $AUTHOR=& $FFPROBE -v quiet -show_entries format_tags=composer -of default=nw=1:nk=1 "$file"
- $TITLE=& $FFPROBE -v quiet -show_entries format_tags=album -of default=nw=1:nk=1 "$file"
- return @{
- Author=$AUTHOR
- Title=$TITLE
- Series=""
- Index=""
- }
- }
- function ParseFilenameFallback($file) {
- $name=[IO.Path]::GetFileNameWithoutExtension($file)
- if ($name -match "(.+?) - (.+)") {
- return @{
- Author=$Matches[1]
- Title=$Matches[2]
- Series=""
- Index=""
- }
- }
- return @{
- Author="Unknown Author"
- Title=$name
- Series=""
- Index=""
- }
- }
- Get-ChildItem $ROOT -Recurse -File |
- Where-Object { $_.Extension -match "\.(epub|azw3|m4b|pdf)$" } |
- ForEach-Object {
- $file=$_.FullName
- $ext=$_.Extension.ToLower()
- if ($ext -eq ".epub" -or $ext -eq ".azw3") {
- $meta=Get-EpubMetadata $file
- }
- elseif ($ext -eq ".m4b") {
- $meta=Get-M4BMetadata $file
- }
- else {
- $meta=ParseFilenameFallback $file
- }
- $AUTHOR=$meta.Author
- $TITLE=$meta.Title
- $INDEX=$meta.Index
- if ([string]::IsNullOrWhiteSpace($AUTHOR)) {
- $AUTHOR="Unknown Author"
- }
- if ([string]::IsNullOrWhiteSpace($TITLE)) {
- $TITLE=$_.BaseName
- }
- # SERIES FROM FOLDER NAME
- $SERIES = Split-Path $_.DirectoryName -Leaf
- $AUTHOR=NormalizeAuthor $AUTHOR
- $AUTHOR=Sanitize $AUTHOR
- $SERIES=Sanitize $SERIES
- $TITLE=Sanitize $TITLE
- if ($INDEX) {
- $num=[int][math]::Round([double]$INDEX)
- $TITLE="{0:D2} - $TITLE" -f $num
- }
- $DEST = Join-Path $ROOT "$AUTHOR\$SERIES\$TITLE"
- if (!(Test-Path $DEST)) {
- New-Item -ItemType Directory -Path $DEST -Force | Out-Null
- }
- $destinationFile=Join-Path $DEST $_.Name
- if (Test-Path $destinationFile) {
- Write-Host "Duplicate skipped:"
- Write-Host " $file"
- return
- }
- Write-Host ""
- Write-Host "Moving:"
- Write-Host " $file"
- Write-Host " → $destinationFile"
- Move-Item -LiteralPath $file -Destination $destinationFile
- }
- Get-ChildItem $ROOT -Recurse -Directory |
- Sort-Object FullName -Descending |
- ForEach-Object {
- if (-not (Get-ChildItem $_.FullName)) {
- Remove-Item $_.FullName -Force
- Write-Host "Deleted empty folder: $($_.FullName)"
- }
- }
- Write-Host ""
- Write-Host "Library organization complete."
Advertisement
Add Comment
Please, Sign In to add comment