anewjose

convert audiobook or eBook folder to ABS compatible (windows)

Mar 14th, 2026
90
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #############################################################
  2. # Requirements: FFmpeg for audiobooks and Calibre for eBooks
  3.  
  4. # Run this command in Powershell to execute the script:
  5. # -ExecutionPolicy Bypass -File .\thisFile.ps1
  6. #############################################################
  7.  
  8. [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
  9. Add-Type -AssemblyName System.IO.Compression.FileSystem
  10. $ROOT = Get-Location
  11. Get-ChildItem -Path $ROOT -Directory -Recurse |
  12. Where-Object { $_.Name -eq "!Side Stories" } |
  13. ForEach-Object {
  14.     $parentName = $_.Parent.Name
  15.     $newName = "$parentName - Side stories"
  16.  
  17.     Rename-Item -Path $_.FullName -NewName $newName
  18. }
  19.  
  20. $FFPROBE = "ffprobe.exe"
  21. # if ffprobe.exe is not in Windows PATH, use
  22. # $FFPROBE="location/of/ffprobe.exe"
  23.  
  24.  
  25. function Sanitize($text) {
  26.     if ([string]::IsNullOrWhiteSpace($text)) { return $text }
  27.     $text = $text -replace '[<>:"/\\|?*]', ' '
  28.     $text = $text.Trim().TrimEnd('.')
  29.     return $text
  30. }
  31.  
  32. function NormalizeAuthor($author) {
  33.     if ($author -match "(.+),\s*(.+)") {
  34.         return "$($Matches[2]) $($Matches[1])"
  35.     }
  36.     return $author
  37. }
  38.  
  39. function Get-EpubMetadata($file) {
  40.  
  41.     $AUTHOR = ""
  42.     $TITLE = ""
  43.     $SERIES = ""
  44.     $INDEX = ""
  45.  
  46.     try {
  47.  
  48.         $zip = [System.IO.Compression.ZipFile]::OpenRead($file)
  49.  
  50.         # container.xml tells us where the OPF is
  51.         $containerEntry = $zip.GetEntry("META-INF/container.xml")
  52.         $reader = New-Object IO.StreamReader($containerEntry.Open())
  53.         $containerXml = [xml]$reader.ReadToEnd()
  54.         $reader.Close()
  55.  
  56.         $opfPath = $containerXml.container.rootfiles.rootfile.'full-path'
  57.         $opfEntry = $zip.GetEntry($opfPath)
  58.  
  59.         $reader = New-Object IO.StreamReader($opfEntry.Open())
  60.         $opfXml = [xml]$reader.ReadToEnd()
  61.         $reader.Close()
  62.  
  63.         $zip.Dispose()
  64.  
  65.         # Set up namespace manager for dc: and opf: namespaces
  66.         $nsmgr = New-Object System.Xml.XmlNamespaceManager($opfXml.NameTable)
  67.         $nsmgr.AddNamespace("dc","http://purl.org/dc/elements/1.1/")
  68.         $nsmgr.AddNamespace("opf","http://www.idpf.org/2007/opf")
  69.  
  70.         $metadataNode = $opfXml.SelectSingleNode("//opf:metadata", $nsmgr)
  71.  
  72.         # Title
  73.         $titleNode = $metadataNode.SelectSingleNode("dc:title", $nsmgr)
  74.         if ($titleNode) { $TITLE = $titleNode.InnerText }
  75.  
  76.         # Authors (handle multiple)
  77.         $authorNodes = $metadataNode.SelectNodes("dc:creator", $nsmgr)
  78.         if ($authorNodes) {
  79.             $AUTHOR = ($authorNodes | ForEach-Object { $_.InnerText }) -join ", "
  80.         }
  81.  
  82.         # Series and Index from Calibre meta tags
  83.         $metaNodes = $metadataNode.SelectNodes("opf:meta", $nsmgr)
  84.         foreach ($m in $metaNodes) {
  85.             if ($m.GetAttribute("name") -eq "calibre:series") { $SERIES = $m.GetAttribute("content") }
  86.             if ($m.GetAttribute("name") -eq "calibre:series_index") { $INDEX = $m.GetAttribute("content") }
  87.         }
  88.  
  89.     } catch {
  90.         # Fail silently
  91.     }
  92.  
  93.     return @{
  94.         Author = $AUTHOR
  95.         Title = $TITLE
  96.         Series = $SERIES
  97.         Index = $INDEX
  98.     }
  99. }
  100.  
  101. function Get-M4BMetadata($file) {
  102.  
  103.     $AUTHOR=& $FFPROBE -v quiet -show_entries format_tags=composer -of default=nw=1:nk=1 "$file"
  104.     $TITLE=& $FFPROBE -v quiet -show_entries format_tags=album -of default=nw=1:nk=1 "$file"
  105.  
  106.     return @{
  107.         Author=$AUTHOR
  108.         Title=$TITLE
  109.         Series=""
  110.         Index=""
  111.     }
  112.  
  113. }
  114.  
  115. function ParseFilenameFallback($file) {
  116.  
  117.     $name=[IO.Path]::GetFileNameWithoutExtension($file)
  118.  
  119.     if ($name -match "(.+?) - (.+)") {
  120.  
  121.         return @{
  122.             Author=$Matches[1]
  123.             Title=$Matches[2]
  124.             Series=""
  125.             Index=""
  126.         }
  127.  
  128.     }
  129.  
  130.     return @{
  131.         Author="Unknown Author"
  132.         Title=$name
  133.         Series=""
  134.         Index=""
  135.     }
  136.  
  137. }
  138.  
  139. Get-ChildItem $ROOT -Recurse -File |
  140. Where-Object { $_.Extension -match "\.(epub|azw3|m4b|pdf)$" } |
  141. ForEach-Object {
  142.  
  143.     $file=$_.FullName
  144.     $ext=$_.Extension.ToLower()
  145.  
  146.     if ($ext -eq ".epub" -or $ext -eq ".azw3") {
  147.         $meta=Get-EpubMetadata $file
  148.     }
  149.     elseif ($ext -eq ".m4b") {
  150.         $meta=Get-M4BMetadata $file
  151.     }
  152.     else {
  153.         $meta=ParseFilenameFallback $file
  154.     }
  155.  
  156.     $AUTHOR=$meta.Author
  157.     $TITLE=$meta.Title
  158.     $INDEX=$meta.Index
  159.  
  160.     if ([string]::IsNullOrWhiteSpace($AUTHOR)) {
  161.         $AUTHOR="Unknown Author"
  162.     }
  163.  
  164.     if ([string]::IsNullOrWhiteSpace($TITLE)) {
  165.         $TITLE=$_.BaseName
  166.     }
  167.  
  168.     # SERIES FROM FOLDER NAME
  169.     $SERIES = Split-Path $_.DirectoryName -Leaf
  170.  
  171.     $AUTHOR=NormalizeAuthor $AUTHOR
  172.  
  173.     $AUTHOR=Sanitize $AUTHOR
  174.     $SERIES=Sanitize $SERIES
  175.     $TITLE=Sanitize $TITLE
  176.  
  177.     if ($INDEX) {
  178.  
  179.         $num=[int][math]::Round([double]$INDEX)
  180.         $TITLE="{0:D2} - $TITLE" -f $num
  181.  
  182.     }
  183.  
  184.     $DEST = Join-Path $ROOT "$AUTHOR\$SERIES\$TITLE"
  185.  
  186.     if (!(Test-Path $DEST)) {
  187.         New-Item -ItemType Directory -Path $DEST -Force | Out-Null
  188.     }
  189.  
  190.     $destinationFile=Join-Path $DEST $_.Name
  191.  
  192.     if (Test-Path $destinationFile) {
  193.  
  194.         Write-Host "Duplicate skipped:"
  195.         Write-Host " $file"
  196.         return
  197.  
  198.     }
  199.  
  200.     Write-Host ""
  201.     Write-Host "Moving:"
  202.     Write-Host " $file"
  203.     Write-Host " → $destinationFile"
  204.  
  205.     Move-Item -LiteralPath $file -Destination $destinationFile
  206.  
  207. }
  208.  
  209. Get-ChildItem $ROOT -Recurse -Directory |
  210. Sort-Object FullName -Descending |
  211. ForEach-Object {
  212.  
  213.     if (-not (Get-ChildItem $_.FullName)) {
  214.  
  215.         Remove-Item $_.FullName -Force
  216.         Write-Host "Deleted empty folder: $($_.FullName)"
  217.  
  218.     }
  219.  
  220. }
  221.  
  222. Write-Host ""
  223. Write-Host "Library organization complete."
Advertisement
Add Comment
Please, Sign In to add comment