ラジコプレミアム:タイムフリー番組を録音するVisual Basic サンプル

前回のメモで、ラジコ無料会員、未登録利用者がIPアドレスに対応した聴取エリアの番組を録音するVisual Basic サンプルプログラムを紹介してきました。

今回は、すでに紹介している、ラジコプレミアムログインサンプルと、無料ダウンロードサンプルの合体。
その他、会員情報と設定を .ini ファイルに格納しておいて、そこから情報を読み込む機能、およびラジコプレミアムログイン状態からログアウトする機能を組み込み、サンプルプログラムを完成させたいと思います。

まずは、Visual Studio のデバッグ機能を設定します。
前回までは、コマンドラインの引数として与えるパラメータを、変数としてプログラム内に定義していましたけど、ほぼ完成に近づいてきたので、ちゃんとコマンドラインから与えようと思います。正確にはコマンドラインから与えた場合と同様に扱う機能がVisual Studio 2019 にはあるため、それを利用します。

具体的にはプロジェクトのプロパティー画面のデバッグ項目で、アプリケーション引数 というフィールドを見つけ、そこにコマンドラインから与えるパラメータを設定することで、コマンドラインをエミュレートできるという具合。これは便利です。

visual studio のデバッグ機能、コマンドラインを代替

コマンドパラメータを入力した後、System.Environment.GetCommandLineArgs() を使って、コマンドライン情報、コマンド+オプションをプログラムに取り込み、For Each を使って、解析。解説は、別のメモに譲ります。

続いて、ini ファイルから設定を読み込みます。

ファイルの読み方に関しては、以前のメモに紹介済み。Radikoプレミアムのログイン情報とffmpeg の所在を定義したconfig.iniの読み取りサンプルは次の通り。いつものように Console.WriteLine はデバッグ用。

        'configファイルの読み込み、アカウントと ffmpeg を環境にセット
        Dim config As String = ""
        Using readfile = New StreamReader("./config.ini")
            config = readfile.ReadToEnd()
            Dim config_line() As String = config.Split(vbCrLf)
            For Each line In config_line
                If (line.StartsWith("#")) Then
                    'Console.WriteLine("Comment Line")
                ElseIf line.Contains("mailaddr=") = True Then
                    mail = line.Replace("mailaddr=", "")
                    'Console.WriteLine(mail)
                ElseIf line.Contains("password=") = True Then
                    pass = line.Replace("password=", "")
                    'Console.WriteLine(pass)
                ElseIf line.Contains("ffmpeg_path=") = True Then
                    ffmpeg_path = line.Replace("ffmpeg_path=", "")
                    'Console.WriteLine(ffmpeg_path)
                End If
            Next
        End Using

config.ini サンプル

# サンプル config.ini
# すべて半角文字で記述のこと。
# ' や " で文字を囲まない。
# 各行の文字間にスペースは入れないこと。空白行はOK。
mailaddr=radiko@example.com
password=timefree
# ffmpeg.exe を同一フォルダーに置く場合は、ffmpeg_path をコメント行にすること
#ffmpeg_path=

ここで解説しておかないといけないのが、config.Split(vbCrLf) 。これは、config.ini ファイルを「Windowsの」改行コードCrLf 毎に分割して変数に取り込むためのものです。
行頭が # で始まる行をコメント行と判断するため StartWith() を使いました。
目的の変数から値を取り出すのに使ったのが、ここでも Replace() 。Replaceメソッド、便利じゃ~。

次に解説するのが、ラジコプレミアム用のAuth2レスポンス。
Auth2レスポンスは、無料会員とラジコプレミアム会員で、サーバーに送る情報が異なるので、それぞれ用のURLを準備しておいて、areafree の値を判断して、HTTPレスポンスの内容をスイッチするように調整しました。

        'Auth1同様、curlコマンドから引き渡すパラメータをいったん分解して定義し、あとで組み立てるようにします。
        'Auth1とパラメータが共通なオプションは、ここでは定義せず、Auth1で定義したものを再利用します。
        Dim auth2_para1 As String = "curl.exe -c cookie.txt -H ""X-Radiko-AuthToken: " & x_token & """ "
        Dim auth2_para2 As String = "-H ""X-Radiko-PartialKey: " & partial_key & """ "
        'ラジコプレミアム用のURL
        Dim auth2_para3 As String = "-L https://radiko.jp/v2/api/auth2?radiko_session=" & radiko_session
        '無料会員用のURL
        Dim auth2_para4 As String = "-L https://radiko.jp/v2/api/auth2"
        ' curl コマンドを合成、上がラジコプレミアム、下が無料用
        Dim auth2_curl As String = ""
        If areafree = "1" Then
            auth2_curl = auth2_para1 & auth2_para2 & auth1_para4 & auth1_para5 & auth2_para3
        Else
            auth2_curl = auth2_para1 & auth2_para2 & auth1_para4 & auth1_para5 & auth2_para4
        End If

上記がそのサンプル。
単純に、areafree:1 の時、radiko_session パラメータを一緒に送り返すだけの違いです。
これで、Auth2認証が完了すれば、Status 200 が返ってきます。

そして、最後が、ラジコからのログオフ。
このプログラムは完成度が低いため、1番組をダウンロードすると終了します。
ダウンロードが終わってそのままにしておくと、(おそらく)しばらくログインしたままになります。複数回ダウンロードするとログイン数の最大値に達してしまう可能性があります。
なので、ダウンロードが終了すると、ログオフするプロトコルを実行します。

それが以下。

        'ダウンロード終了処理

        If areafree = "1" Then
            Dim logout_para1 As String = "curl.exe -s -c cookie.txt -request POST "
            Dim logout_para2 As String = "--data-urlencode ""radiko_session=" & radiko_session & """ "
            Dim logout_para3 As String = """https://radiko.jp/v4/api/member/logout"""
            Dim logout As String = logout_para1 & logout_para2 & logout_para3

            p.StartInfo.Arguments = "/c" & logout
            p.Start()
            results = p.StandardOutput.ReadToEnd()
            Console.WriteLine(logout)
            Console.WriteLine()
            Console.WriteLine(results)
            'プロセス終了まで待機する
            'WaitForExitはReadToEndの後である必要がある
            '(親プロセス、子プロセスでブロック防止のため)
            p.WaitForExit()
            p.Close()

        Else
            'エリアフリーじゃない場合は何もしない
        End If

ここでも、radiko_session を使います。ただし、ここではURLとしてレスポンスするのではなく、POSTメソッドで送ります。200が返ってくれば完了。

以上で、ラジコエリアフリー番組の録音スクリプトの解説は終わり。スクリプトをひとまとめにしたものが次です。

Microsoft から無料のCommunity バージョン Visual Studio 2019か2022をダウンロードして、Visual Basic, Visual C# を使えるようにインストールし、以下のソースをコピーすれば、RadikoPad_cmd が出来上がり、自分用にカスタマイズも可能だと思います。

Imports System.Text
Imports System.IO

Module radikopad_cmd
    Sub Main(args As String())
        Dim ffmpeg_path As String = ".\ffmpeg.exe"
        Dim statsion As String = ""
        Dim start_time As String = ""
        Dim end_time As String = ""
        Dim mail As String = ""
        Dim pass As String = ""
        Dim station_id As String = ""
        Dim rec_from As String = ""
        Dim rec_to As String = ""
        Dim prog_name As String = ""

        Console.WriteLine(System.Environment.CommandLine)
        Dim cmds As String() = System.Environment.GetCommandLineArgs()

        'コマンドライン引数を列挙する
        Dim cmd As String
        Dim i As Integer = 0
        For Each cmd In cmds
            If i = 0 Then
                Dim fullpath As String = System.IO.Path.GetDirectoryName(cmd) ' ワーキングディレクトリ情報

                ffmpeg_path = System.IO.Path.GetDirectoryName(cmd) & "\ffmpeg.exe"
                Console.WriteLine(fullpath)
                If File.Exists(Path.GetDirectoryName(cmd) & "\config.ini") Then
                    'Console.WriteLine("config.iniあり")
                Else
                    'Console.WriteLine("config.iniなし、ファイル作成")
                    Dim fs As FileStream = File.Create(Path.GetDirectoryName(cmd) & "\config.ini")
                    fs.Close()
                    'Console.WriteLine(fs)
                End If
            ElseIf i = 1 Then
                station_id = cmd
            ElseIf i = 2 Then
                rec_from = cmd.Replace("+", "").Replace(":", "") & "00"
                prog_name = station_id & cmd.Replace("+", "").Replace(":", "")
            ElseIf i = 3 Then
                rec_to = cmd.Replace("+", "").Replace(":", "") & "00"
            End If
            Console.WriteLine(i & "=" & cmd)
            i = i + 1
        Next
        'Console.WriteLine(station_id)
        'Console.WriteLine(rec_from)
        'Console.WriteLine(rec_to)

        'configファイルの読み込み、アカウントと ffmpeg を環境にセット
        Dim config As String = ""
        Using readfile = New StreamReader("./config.ini")
            config = readfile.ReadToEnd()
            Dim config_line() As String = config.Split(vbCrLf)
            For Each line In config_line
                If (line.StartsWith("#")) Then
                    'Console.WriteLine("Comment Line")
                ElseIf line.Contains("mailaddr=") = True Then
                    mail = line.Replace("mailaddr=", "")
                    'Console.WriteLine(mail)
                ElseIf line.Contains("password=") = True Then
                    pass = line.Replace("password=", "")
                    'Console.WriteLine(pass)
                ElseIf line.Contains("ffmpeg_path=") = True Then
                    ffmpeg_path = line.Replace("ffmpeg_path=", "")
                    'Console.WriteLine(ffmpeg_path)
                End If
            Next
        End Using

        Dim login1_para1 As String = "curl.exe -c cookie.txt --request POST "
        Dim login1_para2 As String = "--data-urlencode ""mail=" & mail & """ "
        Dim login1_para3 As String = "--data-urlencode ""pass=" & pass & """ "
        Dim login1_para4 As String = """https://radiko.jp/v4/api/member/login"""
        Dim login1 As String = login1_para1 & login1_para2 & login1_para3 & login1_para4
        Dim login1_result As String = ""

        Console.WriteLine(login1)
        Console.WriteLine()

        Dim p As New System.Diagnostics.Process()

        p.StartInfo.FileName = System.Environment.GetEnvironmentVariable("ComSpec")
        p.StartInfo.UseShellExecute = False
        p.StartInfo.RedirectStandardOutput = True
        p.StartInfo.RedirectStandardInput = False
        p.StartInfo.CreateNoWindow = True
        p.StartInfo.Arguments = "/c " & login1

        '変数radiko_sessionを定義して初期化
        Dim radiko_session As String = ""
        '変数 areafree を定義して、初期化
        Dim areafree As String = ""
        Dim results As String = ""
        Dim login As String = ""

        'エリアフリーユーザーかどうか、config.ini のmailアドレスに @ が含まれており、
        '1文字以上のパスワードが設定されているかどうかで判定し、含まれている場合のみ loginする
        If (mail.Contains("@") = True Or pass.Length > 1) Then

            p.Start()

            results = p.StandardOutput.ReadToEnd()

            p.WaitForExit()
            p.Close()

            Console.WriteLine(results)

            login1_result = results
            login = "1"
        Else
            areafree = "0"
            login = "0"
        End If

        'ラジコにログインを送った場合のみ、レスポンス内の情報を処理する
        If login = "1" Then
            'セッション情報を分解
            '不要な文字を削除
            Dim login1_result2 = ((login1_result.Replace("{", "")).Replace("}", "")).Replace("""", "")
            '配列を作って、カンマとカンマの間のフィールドを一つずつ配列に入れる
            Dim l_resp_part() As String = login1_result2.Split(",")
            'For Each とif文をを使って、配列内を一つずつ確認し、radiko_session と areafree を探す。
            '対象の変数名が定義されていれば、変数に取り込む。
            For Each l_resp As String In l_resp_part
                If l_resp.Contains("radiko_session:") = True Then
                    radiko_session = l_resp.Replace("radiko_session:", "")
                    Console.WriteLine("radiko_session:" & radiko_session)
                ElseIf l_resp.Contains("areafree:") = True Then
                    areafree = l_resp.Replace("areafree:", "")
                    Console.WriteLine("areafree:" & areafree)
                End If
            Next
        End If

        'まず、Auth1ようのレスポンスを処理する文字列と入れ物を準備
        ' radiko.jp にアクセスするためのカギが含まれている固定文字列
        Dim _authkey As String = "bcd151073c03b352e1ef2fd66c32209da9ca0afa"
        ' Auth1を行うとレスポンスに含まれる文字列名と、それを小文字で処理するための定義
        Dim str_token As String = "X-Radiko-AuthToken:"
        ' l_ は lower という意味で、変数名の頭に付けています。中身は、x-radiko-authtoken:
        Dim l_str_token As String = str_token.ToLower()
        Dim str_length As String = "X-Radiko-KeyLength:"
        Dim l_str_length As String = str_length.ToLower()
        Dim str_offset As String = "X-Radiko-KeyOffset:"
        Dim l_str_offset As String = str_offset.ToLower()
        '最終的に Auth1 からトークンと、長さとオフセット情報が必要になるので、次の3つの入れ物を準備しておく
        Dim x_token As String = ""
        Dim x_length As Integer = 0
        Dim x_offset As Integer = 0

        Dim auth1_para1 As String = "curl.exe -s -c cookie.txt "
        Dim auth1_para2 As String = "-H ""X-Radiko-App: pc_html5"" "
        Dim auth1_para3 As String = "-H ""X-Radiko-App-Version: 0.0.1"" "
        Dim auth1_para4 As String = "-H ""X-Radiko-User: dummy_user"" "
        Dim auth1_para5 As String = "-H ""X-Radiko-Device: pc"" "
        Dim auth1_para6 As String = "-I -L https://radiko.jp/v2/api/auth1"
        Dim auth1_curl As String = auth1_para1 & auth1_para2 & auth1_para3 & auth1_para4 & auth1_para5 & auth1_para6

        'Dim p As New Process()
        'Windows環境変数から ComSpec(cmd.exe)のパスを取得して、FileNameにセット
        p.StartInfo.FileName = System.Environment.GetEnvironmentVariable("ComSpec")
        '出力を読み取れるようにする
        p.StartInfo.UseShellExecute = False
        p.StartInfo.RedirectStandardOutput = True
        p.StartInfo.RedirectStandardInput = False
        'ウィンドウを表示しないようにする
        p.StartInfo.CreateNoWindow = True

        'コマンドラインを指定("/c"は実行後閉じるために必要)
        p.StartInfo.Arguments = "/c" & auth1_curl
        'デバッグ用ライン、どんなコマンドが発行されたかを表示して一行開ける。
        Console.WriteLine(auth1_curl)
        Console.WriteLine()

        '起動
        p.Start()
        'コマンド実行結果を読み取る
        results = p.StandardOutput.ReadToEnd()

        Console.WriteLine(results)

        'ラジコから返ってくるレスポンスを取り込む配列を準備する
        '改行コードまでを一つに文字列とするためにデリミターを vbCr に設定。vbCr はラジコ側の文字コードによります
        'レスポンスを順番に配列に取り込みます。
        Dim heads() As String = results.Split(vbCr)
        '配列内の値を1個ずつチェックして、必要なヘッダーか不要なものかを判断します。不要なものは捨てる。
        For Each head As String In heads
            If head.ToLower.Contains(l_str_token) = True Then
                'Auth1のトークンは、このあと何回も使うので、変数に取り込んで再利用する
                x_token = head.Remove(0, 21)
                Console.WriteLine("Authtoken: " & x_token)
            ElseIf head.ToLower.Contains(l_str_length) = True Then
                x_length = head.Remove(0, 21)
                Console.WriteLine("KeyLength: " & x_length)
            ElseIf head.ToLower.Contains(l_str_offset) = True Then
                x_offset = head.Remove(0, 21)
                Console.WriteLine("KeyOffset: " & x_offset)
            End If
        Next
        '
        Dim partial_key As String = Convert.ToBase64String(Encoding.UTF8.GetBytes(_authkey.Substring(x_offset, x_length)))
        Console.WriteLine("PartialKey: " & partial_key)
        Console.WriteLine()

        'Auth1同様、curlコマンドから引き渡すパラメータをいったん分解して定義し、あとで組み立てるようにします。
        'Auth1とパラメータが共通なオプションは、ここでは定義せず、Auth1で定義したものを再利用します。
        Dim auth2_para1 As String = "curl.exe -c cookie.txt -H ""X-Radiko-AuthToken: " & x_token & """ "
        Dim auth2_para2 As String = "-H ""X-Radiko-PartialKey: " & partial_key & """ "
        'ラジコプレミアム用のURL
        Dim auth2_para3 As String = "-L https://radiko.jp/v2/api/auth2?radiko_session=" & radiko_session
        '無料会員用のURL
        Dim auth2_para4 As String = "-L https://radiko.jp/v2/api/auth2"
        ' curl コマンドを合成、上がラジコプレミアム、下が無料用
        Dim auth2_curl As String = ""
        If areafree = "1" Then
            auth2_curl = auth2_para1 & auth2_para2 & auth1_para4 & auth1_para5 & auth2_para3
        Else
            auth2_curl = auth2_para1 & auth2_para2 & auth1_para4 & auth1_para5 & auth2_para4
        End If

        p.StartInfo.Arguments = "/c" & auth2_curl

        'トークン返信を起動
        p.Start()
        '出力を読み取る
        results = p.StandardOutput.ReadToEnd()
        'デバッグ用出力
        Console.WriteLine(auth2_curl)
        Console.WriteLine()
        Console.WriteLine(results)
        Console.WriteLine()

        'ffmpegコマンドオプション、録音に必要になる時刻情報、所定のヘッダー情報、録音フォーマットなどを変数に定義
        Dim dl_para1 As String = ffmpeg_path & " -loglevel error -fflags +discardcorrupt "
        Dim dl_para2 As String = "-headers ""X-Radiko-AuthToken: " & x_token & """ "
        Dim dl_para3 As String = " -i ""https://radiko.jp/v2/api/ts/playlist.m3u8?station_id="
        Dim dl_para4 As String = station_id & "&" & "l=15&ft=" & rec_from & "&to=" & rec_to & """"
        Dim dl_para5 As String = " -acodec copy -vn -bsf:a aac_adtstoasc -y " & prog_name & ".m4a"
        '定義した変数を結合して、本番コマンドを作る
        Dim dl_com As String = dl_para1 & dl_para2 & dl_para3 & dl_para4 & dl_para5

        Console.WriteLine(dl_com)

        p.StartInfo.Arguments = "/c" & dl_com

        'トークン返信を起動
        p.Start()
        '出力を読み取る
        results = p.StandardOutput.ReadToEnd()
        'プロセス終了まで待機する
        'WaitForExitはReadToEndの後である必要がある
        '(親プロセス、子プロセスでブロック防止のため)
        p.WaitForExit()
        p.Close()

        'ダウンロード終了処理

        If areafree = "1" Then
            Dim logout_para1 As String = "curl.exe -s -c cookie.txt -request POST "
            Dim logout_para2 As String = "--data-urlencode ""radiko_session=" & radiko_session & """ "
            Dim logout_para3 As String = """https://radiko.jp/v4/api/member/logout"""
            Dim logout As String = logout_para1 & logout_para2 & logout_para3

            p.StartInfo.Arguments = "/c" & logout
            p.Start()
            results = p.StandardOutput.ReadToEnd()
            Console.WriteLine(logout)
            Console.WriteLine()
            Console.WriteLine(results)
            'プロセス終了まで待機する
            'WaitForExitはReadToEndの後である必要がある
            '(親プロセス、子プロセスでブロック防止のため)
            p.WaitForExit()
            p.Close()

        Else
            'エリアフリーじゃない場合は何もしない
        End If

        '出力された結果を表示
        Console.WriteLine(results)

    End Sub
End Module

 

追記(2023/05/10):

Auth2 のステップで、HTTP エラー 405 が発生し、curl.exe のオプション -I を付けていると発生することがわかりました。
上記サンプルを修正し、auth2 のための curl.exe コマンドラインオプション、auth2_para3,auth2_para4 から -I を削除しました。

 

コメントを残す