見出し画像

[BMS] uploader.jp(ux.getuploader.com)の一括自動ダウンロードツールを作りました

[2024/11/07] 年齢確認、サブドメインux以外のアップローダーに対応しました


背景

BMSや差分の配布で使われていることの多いアップローダーですが、ファイル数が多いと全部ダウンロードするのが大変です。例えば、新DPBMS差分アップローダーは1000を超える差分が登録されています。

  • 新DPBMS差分アップローダー

https://ux.getuploader.com/dpbms/

一気にダウンロードしようと、急いでダウンロードリンク開いていくと、エラーが返ってくることもあったと思います。URLは単純な直リンクではないので、一般的なダウンローダーで一括処理することは困難です。

そこで、PowerShellスクリプトでuploader.jp(ux.getuploader.com)の一括ダウンロードツールを作ってみました。

広告収入で成り立っているWebサイトだと思うので、やりすぎは良くないかなと思いますが、さすがに数百のファイルを手動でダウンロードするのは辛いので節度を持って利用すればいいのかなと考えています。

仕様

  • 設定ファイルを利用して、複数のアップローダーを自動で処理できます

  • アップローダー毎にどの番号までダウンロードしたか記録し、再度スクリプトを実行した際は、未ダウンロードのURLから再開します

  • アップローダー別に保存フォルダを仕分けます

  • ファイル名が重複しないように、ダウンロードURL中の番号をファイル名の先頭に付けます({アップローダーID}/download/{番号}、の部分)

  • ダウンロード後のファイルのMD5を計算し、ダウンロードページに記載されているMD5と比較し、ファイルが破損していないか確認します。(稀にサーバー上のデータが破損しているのか一致しないことがあります)

  • アップローダー別にダウンロード結果がどうだったかある程度ログに出力します。

  • ダウンロード済みのファイル(同名のファイル)が保存先に存在する場合、重複ダウンロードしないようになっています(MD5が異なる場合警告を出します)

  • パスワード付きのページはスキップし、その旨をログに出力します

  • サイトの応答がエラーだった場合には、自動でのリトライはしませんが、ログに出力するので、どのファイルで失敗したかは容易に把握できます。

  • 並列ダウンロードは行わず、シリアルに処理します

  • [2024/11/07] 年齢確認アップローダ-に対応しました

  • [2024/11/07] サブドメイン"ux"以外のアップローダーに対応しました

[2024/11/07]
サブドメインについてはuxしか存在しないと勘違いしていましたが色々とあるようなので正規表現を直しました。年齢確認については、確認ボタンを押すというのと、ブラウザのシークレットモードみたいにスクリプト起動中はセッション情報(Cookieとか)を保持することで対応しました。

使い方

PowerShell 7用のスクリプトなので、PowerShell 7で実行するようにしてください。PowerShell 7については以下の記事で軽く触れています。

[2024/11/07] 最新版ダウンロードリンク

旧バージョンも残していますが、おそらく特にデグレてないです。最新版を使えば問題ないと思います。
旧バージョンを使っていた方は「Get-Uploader.ps1」だけ上書きすれば良いと思います。

旧バージョンダウンロードリンク

1. setting.csvを編集し対象アップローダーを設定してください

CSVファイルなので、適当なテキストエディタかExcelで編集できると思います。中身は以下の画像のようになっています。

setting.csvの初期内容

urlには、ダウンロードしたいアップローダーのURLを書いてください。デフォルトではBMS本体や差分が置いてある場所を何行か記載しているので必要に応じて編集してください。
currentIndexは、現在どの番号までダウンロードできているかを保存する列です。基本的にはスクリプトを実行すると自動で更新される値なので手動で変更する必要はありません。必要な場合は手動で書き換えても問題ありません。例: "100"が保存されている場合"101"からダウンロードを試行します。
isDownloadは、そのアップローダをダウンロード対象とするかどうかのフラグです。"0"にするとダウンロードや更新チェックの対象から外れます。長期間更新がないアップローダーなど、一々チェックする必要がないと思った場合に設定を変えることを想定しています。

2. Get-Uploader.ps1冒頭の設定値を変更してください

setting.csvがあるのに、スクリプト本体にべた書きの設定値を変更しなければならないのは、行き当たりばったりで開発したからです。
以下のように、設定用の定数があるので書き換えてください。基本的には$SAVE_DIRだけ変更すればOKです。

# 設定用
$SAVE_DIR = 'E:\保存用\#uploader.jp' # DLするファイルの保存先
$CSV_PATH = "${PSScriptRoot}\setting.csv" # setting.csvの場所、デフォルトだとこの.ps1ファイル自体の場所
$LOG_DIR = "${PSScriptRoot}\log" # ログの出力先、デフォルトだとこの.ps1ファイル自体の場所

3. スクリプトを実行してください

setting.csvに書かれたアップローダーを巡回します。巡回が終わったらsetting.csvのcurrentIndexが更新されます。どのように動作しているかスクショを貼っておきます。

Get-Uploader.ps1実行時の様子

スクリプト全文

# これは「PowerShell 7」で動作確認したスクリプトです
# バージョン v2

# 設定用
$SAVE_DIR = 'E:\保存用\#uploader.jp' # DLするファイルの保存先
$CSV_PATH = "${PSScriptRoot}\setting.csv" # setting.csvの場所、デフォルトだとこの.ps1ファイル自体の場所
$LOG_DIR = "${PSScriptRoot}\log" # ログの出力先、デフォルトだとこの.ps1ファイル自体の場所

# 設定が有効か確認
try {
  @($CSV_PATH) | ForEach-Object { Get-Command $_ -ErrorAction Stop } | Out-Null
  @($SAVE_DIR, $LOG_DIR) | ForEach-Object { if (!(Test-path -LiteralPath $_ -PathType Container)) { throw } } | Out-Null
}
catch {
  Write-Host "[Error] 設定値がおかしいです" -BackgroundColor Red
  Write-Host "スクリプトが完了しました。Enterキーを押すとウィンドウを閉じます。"; Read-Host
  exit
}

# 文字列のHTMLデコード、URLデコードをするために.NETのSystem.Web.HttpUtilityクラスを追加
Add-Type -AssemblyName System.Web

# このスクリプトでInvoke-WebRequestする際のセッション情報を格納する変数(当初不要と思い省略していたが、スクリプト実行中はCookie等を保持するようにした)
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession

# 指定したURLのアップローダ($uploader)の指定した番号($index)のファイルをDLする関数
function Get-File {
  param (
    $index,
    $uploader
  )

  try {
    $id = Split-Path -Path $uploader -Leaf

    # 初回アクセス時のURLを組み立ててrequest
    $uri = "${uploader}download/${index}"
    $response = Invoke-WebRequest -Uri $uri -Method GET -WebSession $session

    # パスワード付きの場合
    if ($response.InputFields | Where-Object { $_.name -eq "password" }) {
      $msg = "${index} : [Warning] パスワード付きのためDownloadできませんでした"
      Write-Host $msg -BackgroundColor Yellow
      $msg | Out-File -LiteralPath "${LOG_DIR}\${id}.log" -Append
      return
    }

    # responseからtokenやMD5を抽出
    $token = $response.InputFields[2].value
    $md5 = $response.InputFields[1].value

    # "ダウンロード"ボタンを押したときと同等のrequest、POSTでtokenを設定
    $response = Invoke-WebRequest -Uri $uri -Method Post -Body @{token = $token } -WebSession $session

    # responseから"Download Now"リンクを抽出、当初必ずtokenが含まれると思っていたが、jpg等の場合tokenなしの直リンクに対応している模様
    # $dlUrlEncoded = $response.Links.href | ForEach-Object {[System.Web.HttpUtility]::HtmlDecode($_)} | Select-String -Pattern $token -Raw
    $dlUrlEncoded = $response.Links.href | ForEach-Object { [System.Web.HttpUtility]::HtmlDecode($_) } | Select-String -Pattern 'downloadx' -Raw
    $dlUrl = [System.Web.HttpUtility]::UrlDecode($dlUrlEncoded)

    # URLからファイル名を取得
    $fileName = Split-Path -Path $dlUrl -Leaf

    # 出力先ディレクトリを作成
    New-Item "${SAVE_DIR}\${id}" -ItemType Directory -Force | Out-Null

    # 既にファイルが存在する場合
    if (Test-Path -LiteralPath "${SAVE_DIR}\${id}\${index}_${fileName}") {
      $filemd5 = Get-FileHash -LiteralPath "${SAVE_DIR}\${id}\${index}_${fileName}" -Algorithm 'MD5' | ForEach-Object { $_.Hash }
      if ($md5 -eq $filemd5) {
        $msg = "${index} : [Info] 既にファイルが存在しました、正しいMD5です : ${md5}"
        Write-Host $msg
        $msg | Out-File -LiteralPath "${LOG_DIR}\${id}.log" -Append
        return
      }
      else {
        $msg = "${index} : [Error] 既にファイルが存在しますが、ページの記載と実際のファイルでMD5が異なっています [Page] ${md5} : [File] ${filemd5}"
        Write-Host $msg -BackgroundColor Red
        $msg | Out-File -LiteralPath "${LOG_DIR}\${id}.log" -Append
        return
      }
    }

    # "Download Now"リンクを押したときと同等のrequest、ファイルをDLしMD5を計算
    Invoke-WebRequest -Uri $dlUrl -Method GET -OutFile "${SAVE_DIR}\${id}\${index}_${fileName}" -WebSession $session
    $filemd5 = Get-FileHash -LiteralPath "${SAVE_DIR}\${id}\${index}_${fileName}" -Algorithm 'MD5' | ForEach-Object { $_.Hash }

    if ($md5 -eq $filemd5) {
      $msg = "${index} : [Info] Downloadが成功しました、正しいMD5です : ${md5}"
      Write-Host $msg
      $msg | Out-File -LiteralPath "${LOG_DIR}\${id}.log" -Append
      return
    }
    else {
      $msg = "${index} : [Error] Downloadできましたが、ページの記載と実際のファイルでMD5が異なっています [Page] ${md5} : [File] ${filemd5}"
      Write-Host $msg -BackgroundColor Red
      $msg | Out-File -LiteralPath "${LOG_DIR}\${id}.log" -Append
      return
    }
  }
  catch {
    # responseのエラーの場合
    if ($_.Exception.Response.StatusCode.value__) {
      $StatusCode = $_.Exception.Response.StatusCode.value__
      if ($StatusCode -eq 404) {
        $msg = "${index} : [Info] Downloadが失敗しました、ページが存在しません : ${StatusCode}"
        Write-Host $msg
        $msg | Out-File -LiteralPath "${LOG_DIR}\${id}.log" -Append
      }
      else {
        $msg = "${index} : [Error] Downloadが失敗しました、サイトの応答がエラーです : ${StatusCode}"
        Write-Host $msg -BackgroundColor Red
        $msg | Out-File -LiteralPath "${LOG_DIR}\${id}.log" -Append
      }
      return
    }
    # その他、想定していないエラーの場合
    $msg = "${index} : [Error] 何らかのエラーが発生しました"
    Write-Host $msg -BackgroundColor Red
    $msg | Out-File -LiteralPath "${LOG_DIR}\${id}.log" -Append
    $_.Exception | Out-Host
    return
  }
}

# $uploaderで指定したアップローダの最新ファイルの番号(index)を取得する関数
# あとついてに、(1) そのアップローダに対する初回アクセスということで、session情報を取得したり、(2) 年齢確認ページの場合、自動で年齢確認してからsession情報を更新したりする
function Get-EndIndex {
  param (
    $uploader
  )
  try {
    $response = Invoke-WebRequest -Uri $uploader -Method GET -WebSession $session
    # 年齢確認ページだった場合
    if ($response.InputFields | Where-Object { $_.value -eq "age_confirmation" }) {
      # "確認"ボタンを押したときと同等のrequest、POSTでパラメータを設定
      $response = Invoke-WebRequest -Uri $uploader -Method Post -Body @{q = "age_confirmation" } -WebSession $session
      Write-Host $uploader " : [Info] 年齢確認ページだったため「確認」ボタンを押下しセッション情報を更新しました。"
    }
    $id = Split-Path -Path $uploader -Leaf
    # $response.Links.hrefを見てしまうとアップローダーの説明欄のリンクにマッチしてしまう可能性があるので、以下のように正規表現でファイル一覧の最新版indexを取得する
    #$regexp = "(?<=\<tr\>\<td\>\<a href\=""https\:\/\/ux\.getuploader\.com\/${id}\/download\/)\d+(?="")"
    # 「u3.getuploader.com」みたいなサイトもあることに気が付いたので以下のように改修
    $regexp = "(?<=\<tr\>\<td\>\<a href\=""https\:\/\/.+?\.getuploader\.com\/${id}\/download\/)\d+(?="")"
    [int]$getindex = $response.Content | Select-String -Pattern $regexp | ForEach-Object { $_.Matches.Value }
    return $getindex
  }
  catch {
    $_.Exception | Out-Host
    return 0
  }
}

# 以下メイン処理
$setting = Import-Csv -LiteralPath "${CSV_PATH}"
$orgSetting = Import-Csv -LiteralPath "${CSV_PATH}"

# アップローダーのURLが重複していないかチェック、重複していると同じファイルを何回もDLする可能性があるのでチェックする
$uniqueUrl = $setting.url | Get-Unique
if ($setting.url.Length -ne $uniqueUrl.Length) {
  Write-Host "[Warning] アップローダーのURLが重複しています、重複が無いようにsetting.csvを修正してください、処理を中止します" -BackgroundColor Yellow
  Write-Host "スクリプトが完了しました。Enterキーを押すとウィンドウを閉じます。"; Read-Host
  exit
}

Write-Host "[Info] アップローダーの最新状況を確認します"
$setting | ForEach-Object {
  try {
    # isDownloadが1の場合のみダウンロード対象アップローダーとする
    if ($_.isDownload -eq 1) {
      Write-Host $_.url " : [Info] 更新を確認します"
      [int]$startIndex = [int]$_.currentIndex + 1 # currentIndexはDL処理済みの最大index、よって+1から試行する
      [int]$endIndex = Get-EndIndex -uploader $_.url
      # endIndex = 0の場合、アップローダーにファイルが存在しないか、アップローダーのURLが間違っているのでError
      if ($endIndex -eq 0) {
        Write-Host $_.url " : [Error] ファイルページのURL番号が1つも取得できませんでした。アップローダーにファイルが1つも存在しないか、アップローダーのURLが間違っています" -BackgroundColor Red
      }
      # アップローダーの最新indexがstartIndex以上の場合はDL処理に入る
      elseif ($endIndex -ge $startIndex) {
        Write-Host $_.url " : [Info] 新しいファイルがあったので ${startIndex} から ${endIndex} までDLします"
        for ($i = $startIndex; $i -le $endIndex; $i++) {
          Get-File -index $i -uploader $_.url
        }
        $_.currentIndex = $endIndex # DLが最後まで終了したのでcurrentIndexをendIndexまで進める
        Write-Host $_.url " : [Info] DLが終了しました"
      }
      else {
        Write-Host $_.url " : [Info] 更新はありませんでした"
      }
    }
  }
  catch {
    Write-Host "[Error] 何らかのエラーが発生したためこのアップローダーの処理を中断します" -BackgroundColor Red
    $_.Exception | Out-Host
  }
}

if (Compare-Object -ReferenceObject $setting.currentIndex -DifferenceObject $orgSetting.currentIndex) {
  $setting | Export-Csv -LiteralPath "${CSV_PATH}"
  Write-Host "[Info] setting.csvを更新しました"
}

Write-Host "[Info] すべての処理が完了しました"

Write-Host "スクリプトが完了しました。Enterキーを押すとウィンドウを閉じます。"; Read-Host

既知の問題

現在(2024/11/07)新たに作成できるアップローダーのIDは重複しないものしか登録できない。なので、保存時はアップローダのIDでフォルダを作成している。
しかし、昔作成された?アップローダーは、サブドメインごとにサーバーが違っていたのか、違うサブドメインでは同一のIDが許容されているように見受けられる。例として以下のようなURLが存在している。

https://ux.getuploader.com/somewhere/
https://u11.getuploader.com/somewhere/

これについては、保存フォルダ作成のprefixにサブドメインもつけるといった対応策が考えられるが、今まで保存していたフォルダまでリネームが必要になってしまう。csvにフォルダ名用のカラムを持たせて、そちらを優先するといった対応方法も考えられる。
いずれにせよ、私個人としては現状の仕様で問題なく運用できているので、改修の優先度は低いです。
お困りの方がもしいればコメントください。コメントいただければこの問題についても対応しようと思います。

この記事が気に入ったらサポートをしてみませんか?