├── .gitignore ├── Dockerfile ├── LICENSE ├── Readme.md ├── ReleaseNotes.md ├── build-386.ps1 ├── build.ps1 ├── changelog.txt ├── livedl-logger.go ├── readme-gen.pl ├── replacelocal.pl ├── src ├── amf │ ├── amf.go │ ├── amf0 │ │ └── amf0.go │ ├── amf3 │ │ └── amf3.go │ └── amf_t │ │ └── amf_t.go ├── buildno │ ├── buildno.go │ └── funcs.go ├── cryptoconf │ └── cryptoconf.go ├── defines │ └── constant.go ├── files │ └── files.go ├── flvs │ └── flv.go ├── gorman │ └── gorman.go ├── httpbase │ └── httpbase.go ├── httpsub │ └── httpsub.go ├── livedl.go ├── log4gui │ └── log4gui.go ├── niconico │ ├── jikken.gox │ ├── nico.go │ ├── nico_db.go │ ├── nico_hls.go │ ├── nico_mem_db.go │ └── nico_rtmp.go ├── objs │ └── objs.go ├── options │ └── options.go ├── procs │ ├── base │ │ └── base.go │ ├── ffmpeg │ │ └── ffmpeg.go │ ├── kill.go │ ├── streamlink │ │ └── streamlink.go │ └── youtube_dl │ │ └── youtube-dl.go ├── rtmps │ ├── message.go │ └── rtmp.go ├── twitcas │ └── twicas.go ├── youtube │ ├── comment.go │ ├── youtube.go │ └── youtube.gox └── zip2mp4 │ └── zip2mp4.go └── updatebuildno.go /.gitignore: -------------------------------------------------------------------------------- 1 | /testrec/ 2 | 3 | *.flv 4 | *.mp4 5 | *.ts 6 | *.mkv 7 | *.part 8 | *.mpg 9 | *.webm 10 | 11 | 12 | *~ 13 | 14 | *.exe 15 | *.dll 16 | *.exe.config 17 | 18 | *.xml 19 | *.m3u8 20 | 21 | *.bin 22 | *.zip 23 | *.txt 24 | *.conf 25 | 26 | *.db 27 | *.pem 28 | *.ass 29 | *.sqlite 30 | *.sqlite3 31 | *-journal 32 | *.sqlite3-shm 33 | *.sqlite3-wal 34 | 35 | !changelog.txt 36 | 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11-alpine as builder 2 | 3 | RUN apk add --no-cache \ 4 | build-base \ 5 | git && \ 6 | go get github.com/gorilla/websocket && \ 7 | go get golang.org/x/crypto/sha3 && \ 8 | go get github.com/mattn/go-sqlite3 && \ 9 | go get github.com/gin-gonic/gin 10 | 11 | COPY . /tmp/livedl 12 | 13 | RUN cd /tmp/livedl && \ 14 | go build src/livedl.go 15 | 16 | 17 | 18 | FROM alpine:3.8 19 | 20 | RUN apk add --no-cache \ 21 | ca-certificates \ 22 | ffmpeg \ 23 | openssl 24 | 25 | COPY --from=builder /tmp/livedl/livedl /usr/local/bin/ 26 | 27 | WORKDIR /livedl 28 | 29 | VOLUME /livedl 30 | 31 | CMD livedl 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 himananiito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | c# livedl 2 | 新配信(HTML5)に対応したニコ生録画ツール。ニコ生以外のサイトにも対応予定 3 | 4 | ## 更新 5 | 6 | [更新履歴](https://github.com/hanaonnao/livedl/blob/master/changelog.txt) 7 | 8 | 9 | 10 | ### 更新方法 11 | 12 | livedlのに移動、livedlを削除します 13 | ``` 14 | rm -rf livedl 15 | ``` 16 | ビルドを再実行し、Livedlビルドする。 17 | 18 | 19 | ## 使い方 20 | https://himananiito.hatenablog.jp/entry/livedl 21 | を参照 22 | 23 | ## Linux(Ubuntu)でのビルド方法 24 | ``` 25 | cat /etc/os-release 26 | NAME="Ubuntu" 27 | VERSION="18.04.4 LTS (Bionic Beaver)" 28 | ``` 29 | 30 | ### Go実行環境のインストール (無い場合) 31 | ``` 32 | wget https://dl.google.com/go/go1.15.11.linux-amd64.tar.gz 33 | sudo rm -rf /usr/local/go 34 | sudo tar -C /usr/local -xzf go1.15.11.linux-amd64.tar.gz 35 | export PATH=$PATH:/usr/local/go/bin 36 | # 必要であれば、bashrcなどにPATHを追加する 37 | ``` 38 | 39 | ### gitをインストール (無い場合) 40 | ``` 41 | sudo apt-get install git 42 | ``` 43 | 44 | ### gccなどのビルドツールをインストール (無い場合) 45 | ``` 46 | sudo apt-get install build-essential 47 | ``` 48 | 49 | ### 必要なgoのモジュールをインストール 50 | ``` 51 | go get github.com/gorilla/websocket 52 | go get golang.org/x/crypto/sha3 53 | go get github.com/mattn/go-sqlite3 54 | go get github.com/gin-gonic/gin 55 | ``` 56 | 57 | ### livedlのソースを取得 58 | ``` 59 | git clone https://github.com/hanaonnao/livedl.git 60 | 61 | (元は git clone https://github.com/himananiito/livedl.git) 62 | ``` 63 | 64 | ### livedlのコンパイル 65 | 66 | ディレクトリを移動 67 | ``` 68 | cd livedl 69 | ``` 70 | 71 | #### ~~(オプション)特定のバージョンを選択する場合~~ 72 | ``` 73 | ~$ git tag 74 | 20180513.6 75 | 20180514.7 76 | ... 77 | 20180729.21 78 | 20180807.22 79 | $ git checkout 20180729.21 (選んだバージョン) 80 | ``` 81 | 82 | #### ~~(オプション)最新のコードをビルドする場合~~ 83 | ``` 84 | git checkout master 85 | ``` 86 | 87 | ビルドする 88 | ``` 89 | go build src/livedl.go 90 | ``` 91 | もし、cannot find package "github.com/gin-gonic/gin" in any of: 92 | 93 | など出る場合は、 94 | `go get github.com/gin-gonic/gin` (適宜読み替える)したのち`go build src/livedl.go`を再実行する 95 | 96 | ``` 97 | ./livedl -h 98 | livedl (20180807.22-linux) 99 | ``` 100 | 101 | ## Windows(32bit及び64bit上での32bit向け)コンパイル方法 102 | 103 | ### gccのインストール 104 | 105 | gcc には必ず以下を使用すること。 106 | 107 | http://tdm-gcc.tdragon.net/download 108 | 109 | 環境変数で(例)`C:\TDM-GCC-64\bin`が他のgccより優先されるように設定すること。 110 | 111 | ### 必要なgoのモジュール 112 | 113 | linuxの説明に倣ってインストールする。 114 | 115 | ### コンパイル 116 | 117 | PowerSellで、`build-386.ps1` を実行する。または以下を実行する。 118 | 119 | ``` 120 | set-item env:GOARCH -value 386 121 | set-item env:CGO_ENABLED -value 1 122 | go build -o livedl.x86.exe src/livedl.go 123 | ``` 124 | 125 | ### 32bit環境で`x509: certificate signed by unknown authority`が出る 126 | 127 | 動けばいいのであればオプションで以下を指定する。 128 | 129 | `-http-skip-verify=on` 130 | 131 | ## Dockerでビルド 132 | 133 | ### livedlのソースを取得 134 | ``` 135 | git clone https://github.com/himananiito/livedl.git 136 | cd livedl 137 | git checkout master # Or another version that supports docker (contains Dockerfile) 138 | ``` 139 | 140 | ### イメージ作成 141 | ``` 142 | docker build -t . 143 | ``` 144 | 145 | ### イメージの使い方 146 | 147 | - 出力フォルダを/livedlにマウント 148 | - 通常のパラメーターに加えて`--no-chdir`を渡す 149 | 150 | ``` 151 | docker run -it --rm -v ~/livedl:/livedl livedl --no-chdir ... 152 | ``` 153 | 154 | 以上 155 | -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | ## 更新履歴/説明 2 | 3 | ### 20200903.39 4 | (https://egg.5ch.net/test/read.cgi/software/1595715643/57) 5 | 6 | ・セルフ追っかけ再生 7 |  例:http://127.0.0.1:12345/m3u8/2/1200/index.m3u8 8 |   現在のシーケンス番号から1200セグメント(リアルタイムの場合30分)戻ったところを再生 9 | 10 | ・追加オプション 11 |  -nico-conv-seqno-start <num> 12 |   MP4への変換を指定したセグメント番号から開始する 13 |  -nico-conv-seqno-end <num> 14 |   MP4への変換を指定したセグメント番号で終了する 15 |  -nico-conv-force-concat 16 |   MP4への変換で画質変更または抜けがあっても分割しないように設定 17 |  -nico-conv-force-concat=on 18 |   (+) 上記を有効に設定 19 |  -nico-conv-force-concat=off 20 |   (+) 上記を無効に設定(デフォルト) 21 | 22 |  -d2h 23 |   [実験的] 録画済みのdb(.sqlite3)を視聴するためのHLSサーバを立てる(-db-to-hls) 24 |    開始シーケンス番号は(変換ではないが) -nico-conv-seqno-start で指定 25 |     使用例:$ livedl lvXXXXXXXXX.sqlite3 -d2h -nico-hls-port 12345 -nico-conv-seqno-start 2780 26 | -------------------------------------------------------------------------------- /build-386.ps1: -------------------------------------------------------------------------------- 1 | 2 | set-item env:GOARCH -value 386 3 | set-item env:CGO_ENABLED -value 1 4 | 5 | go build -o livedl.x86.exe src/livedl.go 6 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | rm livedl.exe 2 | go run updatebuildno.go 3 | go build src/livedl.go 4 | .\build-386.ps1 5 | go build livedl-logger.go 6 | 7 | # hide local path 8 | perl replacelocal.pl 9 | 10 | # Generate Readme.txt 11 | perl readme-gen.pl 12 | 13 | # livedl test run(nico) 14 | $process = Start-Process -FilePath livedl.exe -ArgumentList '-nicotestrun -nicotesttimeout 7 -nicotestfmt "testrec/?UNAME?/?PID?-?UNAME?-?TITLE?"' -PassThru 15 | $process.WaitForExit(1000 * 61) 16 | $process.Kill() 17 | 18 | $process = Start-Process -FilePath livedl.x86.exe -ArgumentList '-nicotestrun -nicotesttimeout 7 -nicotestfmt "testrec/?UNAME?/?PID?-?UNAME?-?TITLE?"' -PassThru 19 | $process.WaitForExit(1000 * 30) 20 | $process.Kill() 21 | 22 | $dir = "livedl" 23 | $zip = "$dir.zip" 24 | if(Test-Path -PathType Leaf $zip) { 25 | rm $zip 26 | } 27 | if(Test-Path -PathType Container $dir) { 28 | rmdir -Recurse $dir 29 | } 30 | mkdir $dir 31 | cp livedl.exe $dir 32 | cp livedl.x86.exe $dir 33 | cp livedl-logger.exe $dir 34 | cp Readme.txt $dir 35 | 36 | cp livedl-gui.exe $dir 37 | cp livedl-gui.exe.config $dir 38 | cp Newtonsoft.Json.dll $dir 39 | cp Newtonsoft.Json.xml $dir 40 | 41 | Compress-Archive -Path $dir -DestinationPath $zip 42 | 43 | if(Test-Path -PathType Container $dir) { 44 | rmdir -Recurse $dir 45 | } 46 | 47 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | 更新履歴/説明 2 | 3 | 20210405.40 4 | 5 | 更新12 (2021/01/31) 6 | ・livedl で-yt-no-streamlink=on -yt-no-youtube-dl=on が指定されたとき、YouTube Live のコメントを永久に取得し続けるパッチ 7 | https://egg.5ch.net/test/read.cgi/software/1595715643/567 8 | 止めたいときは Ctrl-C を入力して少し待てば「comment done」と表示されて終了します 9 | 放送終了後に放置した場合どういう挙動になるかは試してないです 10 | 11 | 更新11 (2021/01/30) 12 | ・livedl を YouTube Live の直近の仕様変更に対応 13 | https://egg.5ch.net/test/read.cgi/software/1595715643/559 14 | 15 | 16 | 更新10 (2021/01/27) 17 | ・金額のフォーマットの要望ないみたいだからこっちで勝手に決めさせてもらったよ 18 | https://egg.5ch.net/test/read.cgi/software/1595715643/543 19 | Youtubeの金額フォーマットを追加(コメントのamount属性) 20 | 21 | 更新9 (2021/01/25) 22 | ・livedl で YouTubeLive リプレイのコメントが取れるよう直したよ 23 | https://egg.5ch.net/test/read.cgi/software/1595715643/523 24 | YouTubeLiveのコメントAPI変更 25 | 26 | 更新8 (2021/01/01) 27 | ・livedl で一部コメントが保存されないのを修正する 28 | https://egg.5ch.net/test/read.cgi/software/1595715643/451 29 | https://egg.5ch.net/test/read.cgi/software/1595715643/457 30 | livedlでvpos属性またはdate_usec属性がないコメントが保存されませんの修正 31 | 32 | 更新7 (2021/01/01) 33 | ・livedl で waybackkey の取得方法を変更する 34 | https://egg.5ch.net/test/read.cgi/software/1595715643/424 35 | 36 | 更新6 (2020/12/13) 37 | ・livedl で YouTube Live を扱えるようにする 38 | patch は livedl.youtube-r1.patch のみ適用 39 | https://egg.5ch.net/test/read.cgi/software/1595715643/402 40 | https://egg.5ch.net/test/read.cgi/software/1595715643/406 41 | 42 | 更新5 (2020/11/15) 43 | ・livedl で HTTP のタイムアウト時間を変更できるようにする 44 | https://egg.5ch.net/test/read.cgi/software/1595715643/272 45 | 46 | 追加オプション 47 |  -http-timeout <num> 48 |   タイムアウト時間(秒)デフォルト: 5秒(最低値) 49 | 50 | 更新4 (2020/10/25) 51 | ・旧配信のタイムシフトを録画できるようにする 52 | https://egg.5ch.net/test/read.cgi/software/1595715643/228 53 | 54 | 更新3(2020/10/06) 55 | patch は livedl.comment-name-attribute-r1.patch.gz のみ適用 56 | https://egg.5ch.net/test/read.cgi/software/1595715643/194 57 | 更新2の修正 58 | 59 | 更新2(2020/10/05) 60 | ・XMLコメントのname属性(出演者が名前付きのコメントする時に使用)を保存するように修正 61 | https://egg.5ch.net/test/read.cgi/software/1595715643/174 62 | 63 | 更新1(2020/10/03) 64 | ・指定時間でタイムシフト録画を停止するためのパッチ(+α) 65 | https://egg.5ch.net/test/read.cgi/software/1595715643/163 66 | 67 | オプション 68 |  -nico-ts-start <num> 69 |   タイムシフトの録画を指定した再生時間(秒)から開始する 70 |  -nico-ts-stop <num> 71 |   タイムシフトの録画を指定した再生時間(秒)で停止する 72 |  上記2つは <分>:<秒> | <時>:<分>:<秒> の形式でも指定可能 73 | 74 |  -nico-ts-start-min <num> 75 |   タイムシフトの録画を指定した再生時間(分)から開始する 76 |  -nico-ts-stop-min <num> 77 |   タイムシフトの録画を指定した再生時間(分)で停止する 78 |  上記2つは <時>:<分> の形式でも指定可能 79 | 80 | 20200903.39 81 | (https://egg.5ch.net/test/read.cgi/software/1595715643/57) 82 | 83 | ・セルフ追っかけ再生 84 |  例:http://127.0.0.1:12345/m3u8/2/1200/index.m3u8 85 |   現在のシーケンス番号から1200セグメント(リアルタイムの場合30分)戻ったところを再生 86 | 87 | ・追加オプション 88 |  -nico-conv-seqno-start <num> 89 |   MP4への変換を指定したセグメント番号から開始する 90 |  -nico-conv-seqno-end <num> 91 |   MP4への変換を指定したセグメント番号で終了する 92 |  -nico-conv-force-concat 93 |   MP4への変換で画質変更または抜けがあっても分割しないように設定 94 |  -nico-conv-force-concat=on 95 |   (+) 上記を有効に設定 96 |  -nico-conv-force-concat=off 97 |   (+) 上記を無効に設定(デフォルト) 98 | 99 |  -d2h 100 |   [実験的] 録画済みのdb(.sqlite3)を視聴するためのHLSサーバを立てる(-db-to-hls) 101 |    開始シーケンス番号は(変換ではないが) -nico-conv-seqno-start で指定 102 |     使用例:$ livedl lvXXXXXXXXX.sqlite3 -d2h -nico-hls-port 12345 -nico-conv-seqno-start 2780 103 | 104 | 2020.07.29 ‐ ニコ生のコメント保存できない問題を修正 105 | https://egg.5ch.net/test/read.cgi/software/1595715643/17 106 | 107 | 2020.07.04 ‐ 「broadcastId not found」エラーへの対処 108 | https://egg.5ch.net/test/read.cgi/software/1570634489/744 109 | 110 | 111 | 2020.06.08 - APIの仕様変更に対応 112 | http://egg.5ch.net/test/read.cgi/software/1570634489/535 113 | 114 | 115 | 116 | 20181215.35 117 | ・-nico-ts-start-minオプションの追加 118 | ・win32bit版のビルドを追加 119 | ・-http-skip-verifyオプションを保存できるようにした 120 | ・ライセンスをMITにした 121 | 122 | 20181107.34 123 | ・[ニコ生] (暫定)TEMPORARILY_CROWDEDで録画終了するようにした 124 | ・ファイル名が半角ドットで終わる場合に全角ドットにした 125 | ・[YouTubeLive] コメントの改行をCRLFにした 126 | ・[ニコ生TS] タイムシフトの録画を指定した再生時間(秒)から開始するオプション追加(merged) 127 | ・[ニコ生TS] 32bitで終了しない問題を修正(merged) 128 | 129 | 20181008.33 130 | ・[Youtube] チャットが取得できない問題を修正 131 | ・[Youtube] Streamlinkでダウンロードできない場合にyoutube-dlを使うようにした 132 | ・[Youtube] コメントファイルを書き出せるようにした。 133 | ・#15 [ニコ生コメント] 出力をCRLFにした。/hbコマンドを出さないオプションを追加 134 | 135 | 20181003.32 136 | ・#14 ★緊急 [ニコ生] 新配信録画のプレイリスト取得にウェイトが入らない問題を修正 137 | ・#9 [ニコ生TS] プレイリストの最後で無限ループしてしまう問題を修正 138 | ・YoutubeLiveコメント対応中(未完了) 139 | ・[実験的] -yt-api-key オプションの追加(未使用) 140 | 141 | 20180925.31 142 | ・#8 [ツイキャス] 「c:」から始まるユーザ名が録画できない問題を修正 143 | ・#11 [ツイキャス] 実行直後またはリトライ中にエラーで終了する問題を修正 144 | ・#10 [ツイキャス] -tcas-retry-intervalが効かない問題を修正 145 | ・#12 [ニコ生] タイムシフトで先頭のセグメント(seqno=0)が取得できない問題を修正 146 | -------------------------------------------------------------------------------- /livedl-logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "bufio" 6 | "regexp" 7 | "sync" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | func main() { 13 | args := os.Args[1:] 14 | var vid string 15 | for _, s := range args { 16 | if ma := regexp.MustCompile(`(lv\d{9,})`).FindStringSubmatch(s); len(ma) > 0 { 17 | vid = ma[1] 18 | } 19 | } 20 | 21 | args = append(args, "-nicoDebug") 22 | cmd := exec.Command("livedl", args...) 23 | 24 | if vid == "" { 25 | cmd.Stdout = os.Stdout 26 | cmd.Stderr = os.Stderr 27 | cmd.Run() 28 | } else { 29 | stdout, err := cmd.StdoutPipe() 30 | if err != nil { 31 | fmt.Println(err) 32 | return 33 | } 34 | stderr, err := cmd.StderrPipe() 35 | if err != nil { 36 | fmt.Println(err) 37 | return 38 | } 39 | 40 | name := fmt.Sprintf("log/%s.txt", vid) 41 | os.MkdirAll("log", os.ModePerm) 42 | f, err := os.Create(name) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | defer f.Close() 48 | 49 | var mtx sync.Mutex 50 | append := func(s string) { 51 | mtx.Lock() 52 | defer mtx.Unlock() 53 | f.WriteString(s) 54 | } 55 | 56 | go func() { 57 | rdr := bufio.NewReader(stdout) 58 | for { 59 | s, err := rdr.ReadString('\n') 60 | if err != nil { 61 | return 62 | } 63 | fmt.Print(s) 64 | append(s) 65 | } 66 | defer stdout.Close() 67 | }() 68 | go func() { 69 | rdr := bufio.NewReader(stderr) 70 | for { 71 | s, err := rdr.ReadString('\n') 72 | if err != nil { 73 | return 74 | } 75 | append(s) 76 | } 77 | defer stderr.Close() 78 | }() 79 | cmd.Run() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /readme-gen.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use v5.20; 4 | 5 | open my $f, "-|", "livedl", "-h" or die; 6 | undef $/; 7 | my $s = <$f>; 8 | close $f; 9 | 10 | $s =~ s{livedl\s*\((\d+\.\d+)[^\r\n]*}{livedl ($1)} or die; 11 | my $ver = $1; 12 | 13 | $s =~ s{chdir:[^\n]*\n}{}; 14 | 15 | open my $g, "changelog.txt" or die; 16 | my $t = <$g>; 17 | close $g; 18 | 19 | $t =~ s{\$latest}{$ver} or die; 20 | 21 | open my $h, ">", "changelog.txt" or die; 22 | print $h $t; 23 | close $h; 24 | 25 | open my $o, ">", "Readme.txt" or die; 26 | say $o $s; 27 | say $o ""; 28 | say $o $t; 29 | close $o; 30 | -------------------------------------------------------------------------------- /replacelocal.pl: -------------------------------------------------------------------------------- 1 | # perl 2 | # livedl.exe内のローカルパスの文字列を隠す 3 | use strict; 4 | use v5.20; 5 | 6 | for my $file("livedl.exe", "livedl.x86.exe", "livedl-logger.exe") { 7 | open my $f, "<:raw", $file or die; 8 | undef $/; 9 | my $s = <$f>; 10 | close $f; 11 | 12 | say "$0: $file"; 13 | 14 | my %h = (); 15 | 16 | while($s =~ m{(?<=\0)[^\0]{5,512}\.go(?=\0)|(?<=[[:cntrl:]])_/[A-Z]_/[^\0]{5,512}}g) { 17 | my $s = $&; 18 | if($s =~ m{\A(.*(?:/Users/.+?/go/src|/Go/src))(/.*)\z}s or 19 | $s =~ m{\A(.*(?=/livedl/src/))(/.*)\z}s) { 20 | my($all, $p, $f) = ($s, $1, $2); 21 | 22 | my $p2 = $p; 23 | $p2 =~ s{.}{*}gs; 24 | #$h{$all} = $p2 . $f; 25 | 26 | #say $p; 27 | $h{$p} = $p2; 28 | } 29 | } 30 | 31 | for my $k (sort{$a cmp $b} keys %h) { 32 | my $k2 = $k; 33 | $k2 =~ s{/}{\\}g; 34 | 35 | my $r = quotemeta $k; 36 | my $r2 = quotemeta $k2; 37 | 38 | say "$k => $h{$k}"; 39 | 40 | $s =~ s{$r}{$h{$k}}g; 41 | $s =~ s{$r2}{$h{$k}}g; 42 | } 43 | 44 | open $f, ">:raw", $file or die; 45 | print $f $s; 46 | close $f; 47 | 48 | sleep 1; 49 | } 50 | -------------------------------------------------------------------------------- /src/amf/amf.go: -------------------------------------------------------------------------------- 1 | package amf 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "./amf0" 7 | "./amf_t" 8 | ) 9 | 10 | func SwitchToAmf3() amf_t.SwitchToAmf3 { 11 | return amf_t.SwitchToAmf3{} 12 | } 13 | 14 | func EncodeAmf0(data []interface{}, asEcmaArray bool) ([]byte, error) { 15 | return amf0.Encode(data, asEcmaArray) 16 | } 17 | 18 | func Amf0EcmaArray(data map[string]interface {}) (amf_t.AMF0EcmaArray) { 19 | return amf_t.AMF0EcmaArray{ 20 | Data: data, 21 | } 22 | } 23 | 24 | // paddingHint: zero padded before AMF data 25 | func DecodeAmf0(data []byte, paddingHint... bool) (res []interface{}, err error) { 26 | rdr := bytes.NewReader(data) 27 | var seek1 bool 28 | for _, h := range paddingHint { 29 | if h { 30 | seek1 = true 31 | break 32 | } 33 | } 34 | if seek1 { 35 | rdr.Seek(1, io.SeekStart) 36 | } 37 | res, err = amf0.DecodeAll(rdr) 38 | if err != nil { 39 | if seek1 { 40 | // retry 41 | rdr.Seek(0, io.SeekStart) 42 | res, err = amf0.DecodeAll(rdr) 43 | } 44 | } 45 | 46 | return 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/amf/amf0/amf0.go: -------------------------------------------------------------------------------- 1 | package amf0 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "encoding/binary" 7 | "math" 8 | "fmt" 9 | "log" 10 | "../amf3" 11 | "../amf_t" 12 | ) 13 | 14 | func encodeNumber(num float64, buff *bytes.Buffer) (err error) { 15 | if err = buff.WriteByte(0); err != nil { 16 | return 17 | } 18 | 19 | bits := math.Float64bits(num) 20 | bytes := make([]byte, 8) 21 | binary.BigEndian.PutUint64(bytes, bits) 22 | if _, err = buff.Write(bytes); err != nil { 23 | return 24 | } 25 | return 26 | } 27 | func encodeBoolean(b bool, buff *bytes.Buffer) (err error) { 28 | if err = buff.WriteByte(1); err != nil { 29 | return 30 | } 31 | 32 | var val byte 33 | if b { 34 | val = 1 35 | } 36 | 37 | if err = buff.WriteByte(val); err != nil { 38 | return 39 | } 40 | 41 | return 42 | } 43 | func encodeUtf8(s string, buff *bytes.Buffer) (err error) { 44 | bs := []byte(s) 45 | if len(bs) > 0xffff { 46 | err = fmt.Errorf("string too large") 47 | return 48 | } 49 | 50 | b0 := make([]byte, 2) 51 | binary.BigEndian.PutUint16(b0, uint16(len(bs))) 52 | if _, err = buff.Write(b0); err != nil { 53 | return 54 | } 55 | if _, err = buff.Write(bs); err != nil { 56 | return 57 | } 58 | return 59 | } 60 | func encodeString(s string, buff *bytes.Buffer) (err error) { 61 | if err = buff.WriteByte(2); err != nil { 62 | return 63 | } 64 | err = encodeUtf8(s, buff) 65 | return 66 | } 67 | func encodeObject(obj map[string]interface {}, buff *bytes.Buffer) (err error) { 68 | if err = buff.WriteByte(3); err != nil { 69 | return 70 | } 71 | 72 | for k, v := range obj { 73 | if err = encodeUtf8(k, buff); err != nil { 74 | return 75 | } 76 | if _, err = encode(v, false, buff); err != nil { 77 | return 78 | } 79 | } 80 | if _, err = buff.Write([]byte{0, 0, 9}); err != nil { 81 | return 82 | } 83 | return 84 | } 85 | 86 | func encodeNull(buff *bytes.Buffer) error { 87 | return buff.WriteByte(5) 88 | } 89 | func encodeSwitchToAmf3(buff *bytes.Buffer) error { 90 | return buff.WriteByte(0x11) 91 | } 92 | func encodeEcmaArray(data map[string]interface {}, buff *bytes.Buffer) (err error) { 93 | if err = buff.WriteByte(8); err != nil { 94 | return 95 | } 96 | buf4 := make([]byte, 4) 97 | binary.BigEndian.PutUint32(buf4, uint32(len(data))) 98 | if _, err = buff.Write(buf4); err != nil { 99 | return 100 | } 101 | 102 | for k, v := range data { 103 | if err = encodeUtf8(k, buff); err != nil { 104 | return 105 | } 106 | if _, err = encode(v, true, buff); err != nil { 107 | return 108 | } 109 | } 110 | if _, err = buff.Write([]byte{0, 0, 9}); err != nil { 111 | return 112 | } 113 | 114 | return 115 | } 116 | func encode(data interface{}, asEcmaArray bool, buff *bytes.Buffer) (toAmf3 bool, err error) { 117 | switch data.(type) { 118 | case string: 119 | err = encodeString(data.(string), buff) 120 | case float64: 121 | err = encodeNumber(data.(float64), buff) 122 | case int: 123 | err = encodeNumber(float64(data.(int)), buff) 124 | case bool: 125 | err = encodeBoolean(data.(bool), buff) 126 | case map[string]interface{}: 127 | if asEcmaArray { 128 | err = encodeEcmaArray(data.(map[string]interface{}), buff) 129 | } else { 130 | err = encodeObject(data.(map[string]interface{}), buff) 131 | } 132 | case []interface {}: 133 | m := make(map[string]interface{}) 134 | for i, d := range data.([]interface {}) { 135 | k := fmt.Sprintf("%d", i) 136 | m[k] = d 137 | } 138 | err = encodeEcmaArray(m, buff) 139 | case nil: 140 | err = encodeNull(buff) 141 | case amf_t.SwitchToAmf3: 142 | toAmf3 = true 143 | err = encodeSwitchToAmf3(buff) 144 | default: 145 | log.Fatalf("amf0/encode %#v", data) 146 | } 147 | return 148 | } 149 | 150 | func Encode(data []interface{}, asEcmaArray bool) (b []byte, err error) { 151 | buff := bytes.NewBuffer(nil) 152 | for i, d := range data { 153 | var toAmf3 bool 154 | if toAmf3, err = encode(d, asEcmaArray, buff); err != nil { 155 | return 156 | } 157 | if toAmf3 { 158 | b2, e := amf3.Encode(data[i+1:]) 159 | if e != nil { 160 | err = e 161 | return 162 | } 163 | b = append(b, buff.Bytes()...) 164 | b = append(b, b2...) 165 | return 166 | } 167 | } 168 | b = buff.Bytes() 169 | return 170 | } 171 | 172 | type objectEnd struct {} 173 | 174 | func decodeString(rdr *bytes.Reader) (str string, err error) { 175 | buf := make([]byte, 2) 176 | if _, err = io.ReadFull(rdr, buf); err != nil { 177 | return 178 | } 179 | len := (int(buf[0]) << 8) | int(buf[1]) 180 | if len > 0 { 181 | buf := make([]byte, len) 182 | if _, err = io.ReadFull(rdr, buf); err != nil { 183 | return 184 | } 185 | str = string(buf) 186 | } 187 | return 188 | } 189 | func decodeNumber(rdr *bytes.Reader) (res float64, err error) { 190 | buf := make([]byte, 8) 191 | if _, err = io.ReadFull(rdr, buf); err != nil { 192 | return 193 | } 194 | 195 | u64 := binary.BigEndian.Uint64(buf) 196 | res = math.Float64frombits(u64) 197 | return 198 | } 199 | func decodeBoolean(rdr *bytes.Reader) (res bool, err error) { 200 | buf := make([]byte, 1) 201 | if _, err = io.ReadFull(rdr, buf); err != nil { 202 | return 203 | } 204 | if buf[0] == 0 { 205 | res = false 206 | } else { 207 | res = true 208 | } 209 | return 210 | } 211 | func decodeObject(rdr *bytes.Reader) (res map[string]interface{}, err error) { 212 | res = make(map[string]interface{}) 213 | for { 214 | key, e := decodeString(rdr) 215 | if e != nil { 216 | err = e 217 | return 218 | } 219 | 220 | val, e := decodeOne(rdr) 221 | if e != nil { 222 | err = e 223 | return 224 | } 225 | if key == "" { 226 | switch val.(type) { 227 | case objectEnd: 228 | return 229 | default: 230 | log.Fatalf("decodeObject: parse error; Not object-end, %+s", val) 231 | } 232 | } 233 | res[key] = val 234 | } 235 | return 236 | } 237 | func decodeEcmaArray(rdr *bytes.Reader) (res map[string]interface{}, err error) { 238 | buf := make([]byte, 4) 239 | if _, err = io.ReadFull(rdr, buf); err != nil { 240 | return 241 | } 242 | //count := binary.BigEndian.Uint32(buf) 243 | //log.Printf("decodeEcmaArray: Count: %v", count) 244 | res, err = decodeObject(rdr) 245 | 246 | return 247 | } 248 | func decodeStrictArray(rdr *bytes.Reader) (res []interface{}, err error) { 249 | buf := make([]byte, 4) 250 | if _, err = io.ReadFull(rdr, buf); err != nil { 251 | return 252 | } 253 | count := binary.BigEndian.Uint32(buf) 254 | for i := uint32(0); i < count; i++ { 255 | re, e := decodeOne(rdr) 256 | if e != nil { 257 | err = e 258 | return 259 | } 260 | res = append(res, re) 261 | } 262 | return 263 | } 264 | 265 | 266 | func decodeOne(rdr *bytes.Reader) (res interface{}, err error) { 267 | buf := make([]byte, 1) 268 | if _, err = io.ReadFull(rdr, buf); err != nil { 269 | return 270 | } 271 | switch buf[0] { 272 | case 0: // Number 273 | res, err = decodeNumber(rdr) 274 | case 1: // Boolean 275 | res, err = decodeBoolean(rdr) 276 | case 2: // String 277 | res, err = decodeString(rdr) 278 | case 3: 279 | res, err = decodeObject(rdr) 280 | case 5: // Null 281 | res = nil 282 | case 6: // undefined 283 | res = nil 284 | case 8: // ECMA Array 285 | res, err = decodeEcmaArray(rdr) 286 | 287 | case 9: // Object End 288 | res = objectEnd{} 289 | case 10: 290 | res, err = decodeStrictArray(rdr) 291 | case 0x11: // Switch to AMF3 292 | dat, e := amf3.DecodeAll(rdr) 293 | if e != nil { 294 | err = e 295 | return 296 | } 297 | res = amf_t.AMF3{Data: dat} 298 | default: 299 | err = fmt.Errorf("Not implemented: type=%d", buf[0]) 300 | } 301 | return 302 | } 303 | 304 | 305 | 306 | func DecodeAll(rdr *bytes.Reader) (res []interface{}, err error) { 307 | for rdr.Len() > 0 { 308 | re, e := decodeOne(rdr) 309 | if e != nil { 310 | err = e 311 | return 312 | } 313 | switch re.(type) { 314 | case amf_t.AMF3: 315 | res = append(res, re.(amf_t.AMF3).Data...) 316 | default: 317 | res = append(res, re) 318 | } 319 | } 320 | return 321 | } 322 | -------------------------------------------------------------------------------- /src/amf/amf3/amf3.go: -------------------------------------------------------------------------------- 1 | package amf3 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | "fmt" 8 | ) 9 | 10 | func decodeU29(rdr *bytes.Reader) (res int, err error) { 11 | for i := 0; i < 4; i++ { 12 | var num byte 13 | if num, err = rdr.ReadByte(); err != nil { 14 | return 15 | } 16 | var flg bool 17 | var val uint8 18 | if i == 3 { 19 | val = num 20 | } else { 21 | flg = (num & 0x80) == 0x80 22 | val = (num & 0x7f) 23 | } 24 | 25 | switch i { 26 | case 0: 27 | res = int(val) 28 | case 3: 29 | res = (res << 8) | int(val) 30 | default: 31 | res = (res << 7) | int(val) 32 | } 33 | if (! flg) { 34 | break 35 | } 36 | } 37 | return 38 | } 39 | 40 | // UTF-8-vr 41 | func decodeString(rdr *bytes.Reader) (str string, err error) { 42 | // UTF-8-vr = U29S-ref 43 | // UTF-8-vr = U29S-value *(UTF8-char) 44 | u29, err := decodeU29(rdr) 45 | if err != nil { 46 | return 47 | } 48 | flag := (u29 & 1) != 0 49 | len := u29 >> 1 50 | 51 | if (! flag) { 52 | // string reference table index 53 | log.Fatalf("[FIXME] not implemented: UTF-8-vr = U29S-ref") 54 | } else { 55 | buf := make([]byte, len) 56 | if _, err = io.ReadFull(rdr, buf); err != nil { 57 | return 58 | } 59 | str = string(buf) 60 | } 61 | return 62 | } 63 | 64 | func assocOrUtf8Empty(rdr *bytes.Reader) (key string, val interface{}, err error) { 65 | key, err = decodeString(rdr) 66 | if err != nil { 67 | return 68 | } 69 | if key == "" { 70 | //fmt.Printf("assocOrUtf8Empty: string is empty\n") 71 | return 72 | } 73 | val, err = decodeOne(rdr) 74 | if err != nil { 75 | return 76 | } 77 | //log.Fatalf("assocOrUtf8Empty: key=%v, val=%v", key, val) 78 | return 79 | } 80 | 81 | func decodeOne(rdr *bytes.Reader) (res interface{}, err error) { 82 | format, err := rdr.ReadByte() 83 | if err != nil { 84 | return 85 | } 86 | switch format { 87 | case 6: // string-marker 88 | res, err = decodeString(rdr) 89 | if err != nil { 90 | return 91 | } 92 | case 9: // array-marker 93 | // array-marker U29O-ref 94 | // # array-marker U29A-value (UTF-8-empty | * (assoc-value) UTF-8-empty) * (value-type) 95 | // array-marker U29A-value * (assoc-value) UTF-8-empty * (value-type) 96 | // array-marker U29A-value UTF-8-empty * (value-type) 97 | u29, _ := decodeU29(rdr) 98 | flag := u29 & 1 != 0 99 | count := u29 >> 1 100 | if (! flag) { 101 | log.Fatalf("[FIXME] not implemented: array-type = array-marker U29O-ref") 102 | } 103 | if count == 0 { // [FIXME] condition OK? 104 | // associative, terminated by empty string 105 | assoc := make(map[string]interface{}) 106 | for { 107 | k, v, e := assocOrUtf8Empty(rdr) 108 | if e != nil { 109 | //fmt.Printf("## amf3 associative: %+v\n", e) 110 | err = e 111 | return 112 | } 113 | if k == "" { 114 | break 115 | } 116 | assoc[k] = v 117 | //log.Printf("AMF3 array: %v = %v", k, v) 118 | } 119 | res = assoc 120 | } 121 | //log.Fatalf("AMF3 array: len: %d", count) 122 | default: 123 | log.Printf("%v\n", res) 124 | log.Fatalf("Not implemented: %d", format) 125 | } 126 | return 127 | } 128 | 129 | func DecodeAll(rdr *bytes.Reader) (res []interface{}, err error) { 130 | for rdr.Len() > 0 { 131 | re, e := decodeOne(rdr) 132 | if e != nil { 133 | err = e 134 | return 135 | } 136 | res = append(res, re) 137 | } 138 | return 139 | } 140 | 141 | 142 | func encodeU29(num int, buff *bytes.Buffer) (err error) { 143 | if (0 <= num && num <= 0x7f) { 144 | if err = buff.WriteByte( byte(num & 0x7f) ); err != nil { 145 | return 146 | } 147 | } else if (0x80 <= num && num <= 0x3fff) { 148 | if err = buff.WriteByte( byte(0x80 | ((num >> 7) & 0x7f)) ); err != nil { 149 | return 150 | } 151 | if err = buff.WriteByte( byte(num & 0x7f) ); err != nil { 152 | return 153 | } 154 | } else if (0x4000 <= num && num <= 0x1fffff) { 155 | if err = buff.WriteByte( byte(0x80 | ((num >> 14) & 0x7f)) ); err != nil { 156 | return 157 | } 158 | if err = buff.WriteByte( byte(0x80 | ((num >> 7) & 0x7f)) ); err != nil { 159 | return 160 | } 161 | if err = buff.WriteByte( byte(num & 0x7f) ); err != nil { 162 | return 163 | } 164 | } else if (0x200000 <= num && num <= 0x3fffffff) { 165 | if err = buff.WriteByte( byte(0x80 | ((num >> 22) & 0x7f)) ); err != nil { 166 | return 167 | } 168 | if err = buff.WriteByte( byte(0x80 | ((num >> 15) & 0x7f)) ); err != nil { 169 | return 170 | } 171 | if err = buff.WriteByte( byte(0x80 | ((num >> 7) & 0x7f)) ); err != nil { 172 | return 173 | } 174 | if err = buff.WriteByte( byte(num & 0xff) ); err != nil { 175 | return 176 | } 177 | } else { 178 | err = fmt.Errorf("u29 overflow") 179 | } 180 | return 181 | } 182 | 183 | func encodeU28Flag(num int, flag bool, buff *bytes.Buffer) (err error) { 184 | if flag { 185 | err = encodeU29(((num << 1) | 1), buff) 186 | } else { 187 | err = encodeU29((num << 1), buff) 188 | } 189 | return 190 | } 191 | 192 | 193 | func encodeArray(data []interface {}, buff *bytes.Buffer) (err error) { 194 | // array-marker 195 | if err = buff.WriteByte(9); err != nil { 196 | return 197 | } 198 | // U29A-value; count of the dense portin of the Array 199 | if err = encodeU28Flag(len(data), true, buff); err != nil { 200 | return 201 | } 202 | // UTF-8-empty 203 | if err = buff.WriteByte(1); err != nil { 204 | return 205 | } 206 | for _, v := range data { 207 | if err = encode(v, buff); err != nil { 208 | return 209 | } 210 | } 211 | return 212 | } 213 | 214 | func encodeStringArray(data []string, buff *bytes.Buffer) error { 215 | var list []interface{} 216 | for _, v := range data { 217 | list = append(list, v) 218 | } 219 | return encodeArray(list, buff) 220 | } 221 | func encodeString(data string, buff *bytes.Buffer) (err error) { 222 | if err = buff.WriteByte(6); err != nil { 223 | return 224 | } 225 | bstr := []byte(data) 226 | // U29S-value 227 | if err = encodeU28Flag(len(bstr), true, buff); err != nil { 228 | return 229 | } 230 | if _, err = buff.Write(bstr); err != nil { 231 | return 232 | } 233 | return 234 | } 235 | 236 | func encode(data interface{}, buff *bytes.Buffer) (err error) { 237 | switch data.(type) { 238 | case string: 239 | err = encodeString(data.(string), buff) 240 | case []string: 241 | err = encodeStringArray(data.([]string), buff) 242 | default: 243 | log.Fatalf("amf0/encode %#v", data) 244 | } 245 | return 246 | } 247 | func Encode(data []interface{}) (b []byte, err error) { 248 | buff := bytes.NewBuffer(nil) 249 | for _, data := range data { 250 | if err = encode(data, buff); err != nil { 251 | return 252 | } 253 | } 254 | b = buff.Bytes() 255 | return 256 | } -------------------------------------------------------------------------------- /src/amf/amf_t/amf_t.go: -------------------------------------------------------------------------------- 1 | package amf_t 2 | 3 | type AMF3 struct { 4 | Data []interface{} 5 | } 6 | 7 | type SwitchToAmf3 struct { 8 | 9 | } 10 | 11 | type AMF0EcmaArray struct { 12 | Data map[string]interface {} 13 | } 14 | -------------------------------------------------------------------------------- /src/buildno/buildno.go: -------------------------------------------------------------------------------- 1 | 2 | package buildno 3 | 4 | var BuildDate = "20210405" 5 | var BuildNo = "40" 6 | -------------------------------------------------------------------------------- /src/buildno/funcs.go: -------------------------------------------------------------------------------- 1 | package buildno 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | func GetBuildNo() string { 9 | return fmt.Sprintf( 10 | "%v.%v-%s-%s", 11 | BuildDate, 12 | BuildNo, 13 | runtime.GOOS, 14 | runtime.GOARCH, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/cryptoconf/cryptoconf.go: -------------------------------------------------------------------------------- 1 | package cryptoconf 2 | 3 | import ( 4 | "golang.org/x/crypto/sha3" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "encoding/json" 12 | "fmt" 13 | "log" 14 | ) 15 | 16 | func Set(dataSet map[string]string, fileName, pass string) (err error) { 17 | var data map[string]interface{} 18 | if _, test := os.Stat(fileName); test == nil { 19 | data, err = Load(fileName, pass) 20 | if err != nil { 21 | return 22 | } 23 | } else { 24 | data = map[string]interface{}{} 25 | } 26 | for key, val := range dataSet { 27 | data[key] = val 28 | } 29 | 30 | digest := sha3.Sum256([]byte(pass)) 31 | block, err := aes.NewCipher(digest[:]) 32 | if err != nil { 33 | log.Fatalln(err) 34 | } 35 | aesgcm, err := cipher.NewGCM(block) 36 | if err != nil { 37 | log.Fatalln(err.Error()) 38 | } 39 | 40 | nonceSize := aesgcm.NonceSize() 41 | // Never use more than 2^32 random nonces with a given key because of the risk of a repeat. 42 | nonce := make([]byte, nonceSize) 43 | if _, err := io.ReadFull(rand.Reader, nonce); err != nil { 44 | log.Fatalln(err.Error()) 45 | } 46 | 47 | plaintext, err := json.Marshal(data) 48 | if err != nil { 49 | return 50 | } 51 | ciphertext := aesgcm.Seal(nonce, nonce, plaintext, nil) 52 | //fmt.Printf("%#v\n", ciphertext) 53 | 54 | file, err := os.Create(fileName) 55 | if err != nil { 56 | return 57 | } 58 | defer file.Close() 59 | if _, err = file.Write(ciphertext); err != nil { 60 | return 61 | } 62 | 63 | return 64 | } 65 | 66 | func Load(file, pass string) (data map[string]interface{}, err error) { 67 | b, err := ioutil.ReadFile(file) 68 | if err != nil { 69 | err = nil 70 | return 71 | } 72 | 73 | digest := sha3.Sum256([]byte(pass)) 74 | block, err := aes.NewCipher(digest[:]) 75 | if err != nil { 76 | log.Fatalln(err) 77 | } 78 | aesgcm, err := cipher.NewGCM(block) 79 | if err != nil { 80 | log.Fatalln(err.Error()) 81 | } 82 | 83 | nonceSize := aesgcm.NonceSize() 84 | 85 | nonce, ciphertext := b[:nonceSize], b[nonceSize:] 86 | 87 | plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) 88 | if err != nil { 89 | err = fmt.Errorf("Password wrong for config: %s", file) 90 | return 91 | } 92 | 93 | ////fmt.Printf("%s\n", plaintext) 94 | data = map[string]interface{}{} 95 | err = json.Unmarshal(plaintext, &data) 96 | 97 | return 98 | } -------------------------------------------------------------------------------- /src/defines/constant.go: -------------------------------------------------------------------------------- 1 | 2 | package defines 3 | 4 | var Twitter = "@himananiito" 5 | var Email = "himananiito@yahoo.co.jp" 6 | -------------------------------------------------------------------------------- /src/files/files.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "regexp" 9 | ) 10 | 11 | func RemoveExtention(fileName string) string { 12 | e := filepath.Ext(fileName) 13 | base := strings.TrimSuffix(fileName, e) 14 | return base 15 | } 16 | func ChangeExtention(fileName, ext string) string { 17 | e := filepath.Ext(fileName) 18 | base := strings.TrimSuffix(fileName, e) 19 | return base + "." + ext 20 | } 21 | 22 | func MkdirByFileName(fileName string) (err error) { 23 | dir := filepath.Dir(fileName) 24 | err = os.MkdirAll(dir, os.ModePerm) 25 | if err != nil { 26 | fmt.Println(err) 27 | return 28 | } 29 | return 30 | } 31 | 32 | func GetFileNameNext(name string) (fileName string, err error) { 33 | fileName = name 34 | _, test := os.Stat(fileName) 35 | if test == nil { 36 | // file Exists 37 | ext := filepath.Ext(fileName) 38 | base := strings.TrimSuffix(fileName, ext) 39 | 40 | var i int 41 | for i = 2; i < 10000000 ; i++ { 42 | fileName = fmt.Sprintf("%s-%d%s", base, i, ext) 43 | _, test := os.Stat(fileName) 44 | if test != nil { 45 | return 46 | } 47 | } 48 | err = fmt.Errorf("too many files: %s", name) 49 | } 50 | return 51 | } 52 | 53 | func ReplaceForbidden(name string) (fileName string) { 54 | fileName = name 55 | fileName = regexp.MustCompile(`\\`).ReplaceAllString(fileName, "¥") 56 | fileName = regexp.MustCompile(`/`).ReplaceAllString(fileName, "∕") 57 | fileName = regexp.MustCompile(`:`).ReplaceAllString(fileName, ":") 58 | fileName = regexp.MustCompile(`\*`).ReplaceAllString(fileName, "*") 59 | fileName = regexp.MustCompile(`\?`).ReplaceAllString(fileName, "?") 60 | fileName = regexp.MustCompile(`"`).ReplaceAllString(fileName, `゛`) 61 | fileName = regexp.MustCompile(`<`).ReplaceAllString(fileName, "<") 62 | fileName = regexp.MustCompile(`>`).ReplaceAllString(fileName, ">") 63 | fileName = regexp.MustCompile(`\|`).ReplaceAllString(fileName, "|") 64 | 65 | fileName = regexp.MustCompile(`)`).ReplaceAllString(fileName, ")") 66 | fileName = regexp.MustCompile(`(`).ReplaceAllString(fileName, "(") 67 | 68 | fileName = regexp.MustCompile(`\p{Zs}+`).ReplaceAllString(fileName, " ") 69 | fileName = regexp.MustCompile(`\A\p{Zs}+|\p{Zs}+\z`).ReplaceAllString(fileName, "") 70 | 71 | // 末尾が.であるようなファイルは作れない 72 | fileName = regexp.MustCompile(`\.\p{Zs}*\z`).ReplaceAllString(fileName, ".") 73 | 74 | return 75 | } -------------------------------------------------------------------------------- /src/flvs/flv.go: -------------------------------------------------------------------------------- 1 | package flvs 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "io" 7 | "encoding/binary" 8 | "bytes" 9 | "bufio" 10 | ) 11 | 12 | type Flv struct { 13 | filename string 14 | file *os.File 15 | writer *bufio.Writer 16 | startAt int 17 | audioTimestamp int 18 | videoTimestamp int 19 | } 20 | func (flv *Flv) Flush() { 21 | if flv.writer != nil { 22 | flv.writer.Flush() 23 | } 24 | } 25 | func (flv *Flv) Close() { 26 | flv.Flush() 27 | if flv.file != nil { 28 | flv.file.Close() 29 | } 30 | } 31 | func Open(name string) (flv *Flv, err error) { 32 | file, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0777) 33 | if err != nil { 34 | return 35 | } 36 | 37 | flv = &Flv { 38 | filename: name, 39 | file: file, 40 | audioTimestamp: -1, 41 | videoTimestamp: -1, 42 | } 43 | 44 | stat, err := file.Stat() 45 | if err != nil { 46 | 47 | } 48 | 49 | // FLV header 50 | sz := stat.Size() 51 | if sz == 0 { 52 | if err = flv.writeHeader(); err != nil { 53 | flv.Close() 54 | return 55 | } 56 | } 57 | 58 | if err = flv.testHeader(); err != nil { 59 | flv.Close() 60 | return 61 | } 62 | 63 | 64 | flv.lastPacketTimestamp() 65 | 66 | if _, err = flv.file.Seek(0, 2); err != nil { 67 | return 68 | } 69 | ts := flv.GetLastTimestamp() 70 | if ts != 0 { 71 | fmt.Printf("[info] Seek point: %d\n", ts) 72 | } 73 | 74 | 75 | flv.writer = bufio.NewWriterSize(file, 256*1024) 76 | 77 | 78 | return 79 | } 80 | func (flv *Flv) AudioExists() bool { 81 | return flv.audioTimestamp >= 0 82 | } 83 | func (flv *Flv) VideoExists() bool { 84 | return flv.videoTimestamp >= 0 85 | } 86 | func (flv *Flv) testHeader() (err error) { 87 | if _, err = flv.file.Seek(0, 0); err != nil { 88 | return 89 | } 90 | 91 | b := make([]byte, 9) 92 | _, err = io.ReadFull(flv.file, b); if err != nil { 93 | return 94 | } 95 | if "FLV" != string(b[0:3]) { 96 | err = fmt.Errorf("magic number is not FLV") 97 | return 98 | } 99 | offset := binary.BigEndian.Uint32(b[5:9]) 100 | flv.startAt = int(offset) 101 | 102 | return 103 | } 104 | 105 | func intToBE24(num int) (data []byte) { 106 | tmp := make([]byte, 4) 107 | binary.BigEndian.PutUint32(tmp, uint32(num)) 108 | data = append(data, tmp[1:]...) 109 | return 110 | } 111 | func intToBE32(num int) (data []byte) { 112 | tmp := make([]byte, 4) 113 | binary.BigEndian.PutUint32(tmp, uint32(num)) 114 | data = append(data, tmp[:]...) 115 | return 116 | } 117 | 118 | func (flv *Flv) writePacket(tag byte, rdr *bytes.Buffer, ts int) (err error) { 119 | buff := bytes.NewBuffer(nil) 120 | 121 | dataSize := intToBE24(rdr.Len()) 122 | tagSize := intToBE32(11 + rdr.Len()) 123 | 124 | // TagType 125 | if err = buff.WriteByte(tag); err != nil { 126 | return 127 | } 128 | // DataSize 129 | if _, err = buff.Write(dataSize); err != nil { 130 | return 131 | } 132 | 133 | // Timestamp 134 | tsBytes := intToBE32(ts) 135 | if _, err = buff.Write(tsBytes[1:4]); err != nil { 136 | return 137 | } 138 | // (TimestampExtended) 139 | if err = buff.WriteByte(tsBytes[0]); err != nil { 140 | return 141 | } 142 | // StreamID 143 | if _, err = buff.Write([]byte{0, 0, 0}); err != nil { 144 | return 145 | } 146 | 147 | // header 148 | if _, err = io.Copy(flv.writer, buff); err != nil { 149 | return 150 | } 151 | // data 152 | if _, err = io.Copy(flv.writer, rdr); err != nil { 153 | return 154 | } 155 | 156 | // PreviousTagSize 157 | if _, err = flv.writer.Write(tagSize); err != nil { 158 | return 159 | } 160 | 161 | return 162 | } 163 | 164 | func (flv *Flv) WriteAudio(rdr *bytes.Buffer, ts int) (err error) { 165 | if ts > flv.audioTimestamp { 166 | flv.audioTimestamp = ts 167 | err = flv.writePacket(8, rdr, ts) 168 | } 169 | return 170 | } 171 | func (flv *Flv) WriteVideo(rdr *bytes.Buffer, ts int) (err error) { 172 | if ts > flv.videoTimestamp { 173 | flv.videoTimestamp = ts 174 | err = flv.writePacket(9, rdr, ts) 175 | } 176 | return 177 | } 178 | func (flv *Flv) WriteMetaData(rdr *bytes.Buffer, ts int) (err error) { 179 | err = flv.writePacket(18, rdr, ts) 180 | return 181 | } 182 | 183 | func (flv *Flv) GetLastTimestamp() int { 184 | var min int 185 | if flv.audioTimestamp > flv.videoTimestamp { 186 | min = flv.videoTimestamp 187 | } else { 188 | min = flv.audioTimestamp 189 | } 190 | if min < 0 { 191 | return 0 192 | } 193 | return min 194 | } 195 | 196 | func (flv *Flv) lastPacketTimestamp() (err error) { 197 | defer flv.file.Seek(0, 2) 198 | 199 | if _, err = flv.file.Seek(-4, 2); err != nil { 200 | fmt.Printf("flv.lastPacketTimestamp: %#v\n", err) 201 | return 202 | } 203 | 204 | b0 := make([]byte, 4) 205 | b1 := make([]byte, 11) 206 | 207 | var audioFound bool 208 | var videoFound bool 209 | for !(audioFound && videoFound) { 210 | _, err = io.ReadFull(flv.file, b0); if err != nil { 211 | return 212 | } 213 | size := binary.BigEndian.Uint32(b0) 214 | //fmt.Printf("size: %d\n", size) 215 | if size == 0 { 216 | break 217 | } 218 | 219 | if _, err = flv.file.Seek(-(int64(size) + 4), 1); err != nil { 220 | return 221 | } 222 | 223 | _, err = io.ReadFull(flv.file, b1); if err != nil { 224 | return 225 | } 226 | ts := 227 | (int(b1[7]) << 24) | 228 | (int(b1[4]) << 16) | 229 | (int(b1[5]) << 8) | 230 | (int(b1[6]) ) 231 | //fmt.Printf("ts: %d\n", ts) 232 | 233 | if b1[0] == 8 { 234 | flv.audioTimestamp = ts 235 | audioFound = true 236 | } else if b1[0] == 9 { 237 | flv.videoTimestamp = ts 238 | videoFound = true 239 | } 240 | 241 | if _, err = flv.file.Seek(-(11 + 4), 1); err != nil { 242 | return 243 | } 244 | 245 | } 246 | 247 | return 248 | } 249 | 250 | func (flv *Flv) writeHeader() (err error) { 251 | _, err = flv.file.Write([]byte{ 252 | 'F', 'L', 'V', 253 | 1, // FLV version 1 254 | 5, // Audio+Video tags are present 255 | 0, 0, 0, 9, // DataOffset = 9 256 | 0, 0, 0, 0, // PreviousTagSize0 257 | }) 258 | return 259 | } 260 | -------------------------------------------------------------------------------- /src/gorman/gorman.go: -------------------------------------------------------------------------------- 1 | package gorman 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type GoroutineManager struct { 8 | channels map[chan struct{}] struct{} 9 | mtxChan sync.Mutex 10 | 11 | mtxWg sync.Mutex 12 | wg sync.WaitGroup 13 | 14 | codeChecker func(code int) 15 | } 16 | 17 | func NewManager() *GoroutineManager { 18 | return &GoroutineManager{ 19 | channels: map[chan struct{}] struct{}{}, 20 | } 21 | } 22 | func WithChecker(f func(int)) *GoroutineManager { 23 | return &GoroutineManager{ 24 | channels: map[chan struct{}] struct{}{}, 25 | codeChecker: f, 26 | } 27 | } 28 | func (gm *GoroutineManager) addChan(c chan struct{}) { 29 | gm.mtxChan.Lock() 30 | defer gm.mtxChan.Unlock() 31 | gm.channels[c] = struct{}{} 32 | } 33 | func (gm *GoroutineManager) delChan(c chan struct{}) { 34 | gm.mtxChan.Lock() 35 | defer gm.mtxChan.Unlock() 36 | delete(gm.channels, c) 37 | } 38 | func (gm *GoroutineManager) Cancel() { 39 | gm.mtxChan.Lock() 40 | defer gm.mtxChan.Unlock() 41 | for c, _ := range gm.channels { 42 | close(c) 43 | delete(gm.channels, c) 44 | } 45 | } 46 | func (gm *GoroutineManager) Count() int { 47 | gm.mtxChan.Lock() 48 | defer gm.mtxChan.Unlock() 49 | return len(gm.channels) 50 | } 51 | func (gm *GoroutineManager) Go(f func(<-chan struct{}) int) { 52 | gm.wg.Add(1) 53 | stopChan := make(chan struct{}, 1) 54 | gm.addChan(stopChan) 55 | 56 | go func(){ 57 | defer gm.wg.Done() 58 | code := f(stopChan) 59 | gm.delChan(stopChan) 60 | if gm.codeChecker != nil { 61 | gm.codeChecker(code) 62 | } 63 | }() 64 | } 65 | func (gm *GoroutineManager) RegisterCodeChecker(f func(int)) { 66 | gm.codeChecker = f 67 | } 68 | func (gm *GoroutineManager) Wait() { 69 | gm.wg.Wait() 70 | } 71 | -------------------------------------------------------------------------------- /src/httpbase/httpbase.go: -------------------------------------------------------------------------------- 1 | package httpbase 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "os" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "strings" 11 | "net/http" 12 | "net/url" 13 | "time" 14 | "errors" 15 | "encoding/json" 16 | "encoding/pem" 17 | "bytes" 18 | 19 | "../buildno" 20 | "../defines" 21 | ) 22 | 23 | func GetUserAgent() string { 24 | return fmt.Sprintf( 25 | "livedl/%s (contact: twitter=%s, email=%s)", 26 | buildno.GetBuildNo(), 27 | defines.Twitter, 28 | defines.Email, 29 | ) 30 | } 31 | 32 | var Client = &http.Client{ 33 | Timeout: time.Duration(5) * time.Second, 34 | CheckRedirect: func(req *http.Request, via []*http.Request) (err error) { 35 | if req != nil && via != nil && len(via) > 0 { 36 | if len(via) >= 10 { 37 | return errors.New("stopped after 10 redirects") 38 | } 39 | req.Header = via[0].Header 40 | } 41 | return nil 42 | }, 43 | } 44 | 45 | func checkTransport() bool { 46 | if Client.Transport == nil { 47 | Client.Transport = &http.Transport{} 48 | } 49 | switch Client.Transport.(type) { 50 | case *http.Transport: 51 | return true 52 | } 53 | return false 54 | } 55 | func checkTLSClientConfig() bool { 56 | if !checkTransport() { 57 | return false 58 | } 59 | 60 | if Client.Transport.(*http.Transport).TLSClientConfig == nil { 61 | Client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{} 62 | } 63 | 64 | return true 65 | } 66 | func SetRootCA(file string) (err error) { 67 | if ! checkTLSClientConfig() { 68 | err = fmt.Errorf("SetRootCA: check failed") 69 | return 70 | } 71 | 72 | dat, err := ioutil.ReadFile(file) 73 | if err != nil { 74 | return 75 | } 76 | 77 | // try decode pem 78 | var nDecode int 79 | for len(dat) > 0 { 80 | block, d := pem.Decode(dat) 81 | if block == nil { 82 | break 83 | } 84 | dat = d 85 | nDecode++ 86 | if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { 87 | continue 88 | } 89 | addCert(block.Bytes) 90 | } 91 | if nDecode < 1 { 92 | addCert(dat) 93 | } 94 | 95 | return 96 | } 97 | func addCert(dat []byte) (err error) { 98 | certs, err := x509.ParseCertificates(dat) 99 | if err != nil { 100 | return 101 | } 102 | if certs == nil { 103 | err = fmt.Errorf("ParseCertificates failed") 104 | return 105 | } 106 | 107 | if len(certs) > 0 { 108 | if Client.Transport.(*http.Transport).TLSClientConfig.RootCAs == nil { 109 | Client.Transport.(*http.Transport).TLSClientConfig.RootCAs = x509.NewCertPool() 110 | } 111 | } 112 | 113 | for _, cert := range certs { 114 | Client.Transport.(*http.Transport).TLSClientConfig.RootCAs.AddCert(cert) 115 | } 116 | return 117 | } 118 | 119 | func SetSkipVerify(skip bool) (err error) { 120 | if checkTLSClientConfig() { 121 | Client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = skip 122 | } else { 123 | err = fmt.Errorf("SetSkipVerify(%#v): check failed", skip) 124 | } 125 | return 126 | } 127 | func SetProxy(rawurl string) (err error) { 128 | if ! checkTransport() { 129 | return fmt.Errorf("SetProxy(%#v): check failed", rawurl) 130 | } 131 | 132 | u, err := url.Parse(rawurl) 133 | if err != nil { 134 | return 135 | } 136 | Client.Transport.(*http.Transport).Proxy = http.ProxyURL(u) 137 | return 138 | } 139 | 140 | func httpBase(method, uri string, header map[string]string, body io.Reader) (resp *http.Response, err, neterr error) { 141 | req, err := http.NewRequest(method, uri, body) 142 | if err != nil { 143 | return 144 | } 145 | 146 | req.Header.Set("User-Agent", GetUserAgent()) 147 | 148 | for k, v := range header { 149 | req.Header.Set(k, v) 150 | } 151 | 152 | resp, neterr = Client.Do(req) 153 | if neterr != nil { 154 | if strings.Contains(neterr.Error(), "x509: certificate signed by unknown") { 155 | fmt.Println(neterr) 156 | os.Exit(10) 157 | } 158 | return 159 | } 160 | return 161 | } 162 | func Get(uri string, header map[string]string) (*http.Response, error, error) { 163 | return httpBase("GET", uri, header, nil) 164 | } 165 | func PostForm(uri string, header map[string]string, val url.Values) (*http.Response, error, error) { 166 | if header == nil { 167 | header = make(map[string]string) 168 | } 169 | header["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8" 170 | return httpBase("POST", uri, header, strings.NewReader(val.Encode())) 171 | } 172 | func reqJson(method, uri string, header map[string]string, data interface{}) ( 173 | *http.Response, error, error) { 174 | encoded, err := json.Marshal(data) 175 | if err != nil { 176 | return nil, err, nil 177 | } 178 | 179 | if header == nil { 180 | header = make(map[string]string) 181 | } 182 | header["Content-Type"] = "application/json" 183 | 184 | return httpBase(method, uri, header, bytes.NewReader(encoded)) 185 | } 186 | func PostJson(uri string, header map[string]string, data interface{}) (*http.Response, error, error) { 187 | return reqJson("POST", uri, header, data) 188 | } 189 | func PutJson(uri string, header map[string]string, data interface{}) (*http.Response, error, error) { 190 | return reqJson("PUT", uri, header, data) 191 | } 192 | func PostData(uri string, header map[string]string, data io.Reader) (*http.Response, error, error) { 193 | if header == nil { 194 | header = make(map[string]string) 195 | } 196 | return httpBase("POST", uri, header, data) 197 | } 198 | func GetBytes(uri string, header map[string]string) (code int, buff []byte, err, neterr error) { 199 | resp, err, neterr := Get(uri, header) 200 | if err != nil { 201 | return 202 | } 203 | if neterr != nil { 204 | return 205 | } 206 | defer resp.Body.Close() 207 | 208 | buff, neterr = ioutil.ReadAll(resp.Body) 209 | if neterr != nil { 210 | return 211 | } 212 | 213 | code = resp.StatusCode 214 | 215 | return 216 | } -------------------------------------------------------------------------------- /src/httpsub/httpsub.go: -------------------------------------------------------------------------------- 1 | 2 | package httpsub 3 | 4 | import ( 5 | "net/http" 6 | "os" 7 | "sync" 8 | "log" 9 | "io" 10 | "fmt" 11 | "bytes" 12 | ) 13 | 14 | type SubDownloader struct { 15 | method string 16 | uri string 17 | data []byte 18 | Header map[string]string 19 | RangeSize int64 20 | BuffSize int64 21 | fileName string 22 | file *os.File 23 | numConcurrent int 24 | chRunning chan bool 25 | mtx sync.Mutex 26 | wg sync.WaitGroup 27 | chLength chan int64 28 | } 29 | func (sub *SubDownloader) Concurrent(c int) { 30 | sub.numConcurrent = c 31 | } 32 | func Get(uri, fileName string) (sub *SubDownloader) { 33 | sub = &SubDownloader{ 34 | method: "GET", 35 | uri: uri, 36 | fileName: fileName, 37 | } 38 | return 39 | } 40 | func (sub *SubDownloader) Close() { 41 | sub.mtx.Lock() 42 | defer sub.mtx.Unlock() 43 | if sub.file != nil { 44 | sub.file.Close() 45 | sub.file = nil 46 | } 47 | } 48 | func (sub *SubDownloader) open() { 49 | f, err := os.Create(sub.fileName) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | sub.file = f 54 | } 55 | func (sub *SubDownloader) write(pos int64, rdr io.Reader) (err error) { 56 | sub.mtx.Lock() 57 | defer sub.mtx.Unlock() 58 | //fmt.Printf("write %d\n", pos) 59 | if sub.file == nil { 60 | sub.open() 61 | } 62 | if _, err = sub.file.Seek(pos, 0); err != nil { 63 | log.Fatalln(err) 64 | } 65 | if _, err = io.Copy(sub.file, rdr); err != nil { 66 | log.Fatalln(err) 67 | } 68 | return 69 | } 70 | func (sub *SubDownloader) subrange(pos int64) { 71 | //fmt.Printf("start subrange pos(%d), size(%d) \n", pos, sub.RangeSize) 72 | sub.wg.Add(1) 73 | sub.chRunning <- true 74 | go func() { 75 | defer func() { 76 | <-sub.chRunning 77 | sub.wg.Done() 78 | }() 79 | data := bytes.NewBuffer(sub.data) 80 | req, _ := http.NewRequest(sub.method, sub.uri, data) 81 | req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", pos, pos + sub.RangeSize - 1)) 82 | 83 | client := new(http.Client) 84 | resp, err := client.Do(req) 85 | if err != nil { 86 | return 87 | } 88 | defer resp.Body.Close() 89 | 90 | switch resp.StatusCode { 91 | case 206: 92 | default: 93 | log.Fatalf("StatusCode is %v\n", resp.StatusCode) 94 | } 95 | sub.chLength <- resp.ContentLength 96 | 97 | buff := new(bytes.Buffer) 98 | wbytes := int64(0) 99 | for { 100 | n, _ := io.CopyN(buff, resp.Body, sub.BuffSize) 101 | //fmt.Printf("buff size is %d\n", buff.Len()) 102 | if n > 0 { 103 | sub.write(pos + wbytes, buff) 104 | wbytes += n 105 | } else { 106 | return 107 | } 108 | } 109 | }() 110 | } 111 | func (sub *SubDownloader) Wait() { 112 | sub.chRunning = make(chan bool, sub.numConcurrent) 113 | sub.chLength = make(chan int64, 10) 114 | 115 | if sub.RangeSize <= 0 { 116 | sub.RangeSize = 10*1000*1000 117 | } 118 | 119 | if sub.BuffSize <= 0 { 120 | sub.BuffSize = 3*1000*1000 121 | } 122 | 123 | pos := int64(0) 124 | for { 125 | sub.subrange(pos) 126 | length := <-sub.chLength 127 | fmt.Printf("Downloading %v: %v-%v\n", sub.fileName, pos, pos + length - 1) 128 | if length == sub.RangeSize { 129 | pos += length 130 | } else { 131 | break 132 | } 133 | } 134 | sub.wg.Wait() 135 | sub.Close() 136 | } 137 | -------------------------------------------------------------------------------- /src/livedl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "./options" 9 | "./twitcas" 10 | "./niconico" 11 | "./youtube" 12 | "./zip2mp4" 13 | "time" 14 | "strings" 15 | "./httpbase" 16 | ) 17 | 18 | func main() { 19 | var baseDir string 20 | if regexp.MustCompile(`\AC:\\.*\\Temp\\go-build[^\\]*\\[^\\]+\\exe\\[^\\]*\.exe\z`).MatchString(os.Args[0]) { 21 | // go runで起動時 22 | pwd, e := os.Getwd() 23 | if e != nil { 24 | fmt.Println(e) 25 | return 26 | } 27 | baseDir = pwd 28 | } else { 29 | //pa, e := filepath.Abs(os.Args[0]) 30 | pa, e := os.Executable() 31 | if e != nil { 32 | fmt.Println(e) 33 | return 34 | } 35 | 36 | // symlinkを追跡する 37 | for { 38 | sl, e := os.Readlink(pa) 39 | if e != nil { 40 | break 41 | } 42 | pa = sl 43 | } 44 | baseDir = filepath.Dir(pa) 45 | } 46 | 47 | opt := options.ParseArgs() 48 | 49 | // chdir if not disabled 50 | if !opt.NoChdir { 51 | fmt.Printf("chdir: %s\n", baseDir) 52 | if e := os.Chdir(baseDir); e != nil { 53 | fmt.Println(e) 54 | return 55 | } 56 | } 57 | 58 | // http 59 | if opt.HttpRootCA != "" { 60 | if err := httpbase.SetRootCA(opt.HttpRootCA); err != nil { 61 | fmt.Println(err) 62 | return 63 | } 64 | } 65 | if opt.HttpSkipVerify { 66 | if err := httpbase.SetSkipVerify(true); err != nil { 67 | fmt.Println(err) 68 | return 69 | } 70 | } 71 | if opt.HttpProxy != "" { 72 | if err := httpbase.SetProxy(opt.HttpProxy); err != nil { 73 | fmt.Println(err) 74 | return 75 | } 76 | } 77 | 78 | switch opt.Command { 79 | default: 80 | fmt.Printf("Unknown command: %v\n", opt.Command) 81 | os.Exit(1) 82 | 83 | case "TWITCAS": 84 | var doneTime int64 85 | for { 86 | done, dbLocked := twitcas.TwitcasRecord(opt.TcasId, "") 87 | if dbLocked { 88 | break 89 | } 90 | if (! opt.TcasRetry) { 91 | break 92 | } 93 | 94 | if opt.TcasRetryTimeoutMinute < 0 { 95 | 96 | } else if done { 97 | doneTime = time.Now().Unix() 98 | 99 | } else { 100 | if doneTime == 0 { 101 | doneTime = time.Now().Unix() 102 | } else { 103 | delta := time.Now().Unix() - doneTime 104 | var minutes int 105 | if opt.TcasRetryTimeoutMinute == 0 { 106 | minutes = options.DefaultTcasRetryTimeoutMinute 107 | } else { 108 | minutes = opt.TcasRetryTimeoutMinute 109 | } 110 | 111 | if minutes > 0 { 112 | if delta > int64(minutes * 60) { 113 | break 114 | } 115 | } 116 | } 117 | } 118 | 119 | var interval int 120 | if opt.TcasRetryInterval <= 0 { 121 | interval = options.DefaultTcasRetryInterval 122 | } else { 123 | interval = opt.TcasRetryInterval 124 | } 125 | select { 126 | case <-time.After(time.Duration(interval) * time.Second): 127 | } 128 | } 129 | 130 | case "YOUTUBE": 131 | err := youtube.Record(opt.YoutubeId, opt.YtNoStreamlink, opt.YtNoYoutubeDl) 132 | if err != nil { 133 | fmt.Println(err) 134 | } 135 | 136 | case "NICOLIVE": 137 | hlsPlaylistEnd, dbname, err := niconico.Record(opt); 138 | if err != nil { 139 | fmt.Println(err) 140 | os.Exit(1) 141 | } 142 | if hlsPlaylistEnd && opt.NicoAutoConvert { 143 | done, nMp4s, skipped, err := zip2mp4.ConvertDB(dbname, opt.ConvExt, opt.NicoSkipHb, opt.NicoConvForceConcat, opt.NicoConvSeqnoStart, opt.NicoConvSeqnoEnd) 144 | if err != nil { 145 | fmt.Println(err) 146 | os.Exit(1) 147 | } 148 | if done { 149 | if nMp4s == 1 && (! skipped) { 150 | if 1 <= opt.NicoAutoDeleteDBMode { 151 | os.Remove(dbname) 152 | } 153 | } else if 1 < nMp4s || (nMp4s == 1 && skipped) { 154 | if 2 <= opt.NicoAutoDeleteDBMode { 155 | os.Remove(dbname) 156 | } 157 | } 158 | } 159 | } 160 | case "NICOLIVE_TEST": 161 | if err := niconico.TestRun(opt); err != nil { 162 | fmt.Println(err) 163 | os.Exit(1) 164 | } 165 | 166 | case "ZIP2MP4": 167 | if err := zip2mp4.Convert(opt.ZipFile); err != nil { 168 | fmt.Println(err) 169 | os.Exit(1) 170 | } 171 | 172 | case "DB2MP4": 173 | if strings.HasSuffix(opt.DBFile, ".yt.sqlite3") { 174 | zip2mp4.YtComment(opt.DBFile) 175 | 176 | } else if opt.ExtractChunks { 177 | if _, err := zip2mp4.ExtractChunks(opt.DBFile, opt.NicoSkipHb); err != nil { 178 | fmt.Println(err) 179 | os.Exit(1) 180 | } 181 | 182 | } else { 183 | if _, _, _, err := zip2mp4.ConvertDB(opt.DBFile, opt.ConvExt, opt.NicoSkipHb, opt.NicoConvForceConcat, opt.NicoConvSeqnoStart, opt.NicoConvSeqnoEnd); err != nil { 184 | fmt.Println(err) 185 | os.Exit(1) 186 | } 187 | } 188 | 189 | case "DB2HLS": 190 | if opt.NicoHlsPort == 0 { 191 | fmt.Println("HLS port not specified") 192 | os.Exit(1) 193 | } 194 | if err := zip2mp4.ReplayDB(opt.DBFile, opt.NicoHlsPort, opt.NicoConvSeqnoStart); err != nil { 195 | fmt.Println(err) 196 | os.Exit(1) 197 | } 198 | } 199 | 200 | 201 | return 202 | } 203 | -------------------------------------------------------------------------------- /src/log4gui/log4gui.go: -------------------------------------------------------------------------------- 1 | package log4gui 2 | 3 | import ( 4 | "fmt" 5 | "encoding/json" 6 | ) 7 | 8 | func print(k, v string) { 9 | bs, e := json.Marshal(map[string]string{ 10 | k: v, 11 | }) 12 | if(e != nil) { 13 | fmt.Println(e) 14 | return 15 | } 16 | fmt.Println("$" + string(bs) + "$") 17 | } 18 | func Info(s string) { 19 | print("Info", s) 20 | } 21 | func Error(s string) { 22 | print("Error", s) 23 | } -------------------------------------------------------------------------------- /src/niconico/jikken.gox: -------------------------------------------------------------------------------- 1 | 2 | 3 | package niconico 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "time" 9 | "os/signal" 10 | "syscall" 11 | "net/http" 12 | "io/ioutil" 13 | "log" 14 | "encoding/json" 15 | "bytes" 16 | "../options" 17 | "../obj" 18 | "../files" 19 | ) 20 | 21 | 22 | func getActionTrackId() (actionTrackId string, err error) { 23 | uri := "https://public.api.nicovideo.jp/v1/action-track-ids.json" 24 | req, _ := http.NewRequest("POST", uri, nil) 25 | 26 | req.Header.Set("Content-Type", "application/json") 27 | 28 | client := new(http.Client) 29 | resp, e := client.Do(req) 30 | if e != nil { 31 | err = e 32 | return 33 | } 34 | defer resp.Body.Close() 35 | bs, err := ioutil.ReadAll(resp.Body) 36 | if err != nil { 37 | log.Println(err) 38 | } 39 | 40 | var props interface{} 41 | if err = json.Unmarshal(bs, &props); err != nil { 42 | return 43 | } 44 | 45 | //obj.PrintAsJson(props) 46 | 47 | data, ok := obj.FindString(props, "data") 48 | if (! ok) { 49 | err = fmt.Errorf("actionTrackId not found") 50 | } 51 | actionTrackId = data 52 | 53 | return 54 | } 55 | 56 | func jikkenWatching(opt options.Option, actionTrackId string, isArchive bool) (props interface{}, err error) { 57 | 58 | str, _ := json.Marshal(OBJ{ 59 | "actionTrackId": actionTrackId, 60 | "isBroadcaster": false, 61 | "isLowLatencyStream": true, 62 | "streamCapacity": "superhigh", 63 | "streamProtocol": "https", 64 | "streamQuality": "auto", // high, auto 65 | }) 66 | if err != nil { 67 | log.Println(err) 68 | return 69 | } 70 | 71 | data := bytes.NewReader(str) 72 | 73 | var uri string 74 | if isArchive { 75 | uri = fmt.Sprintf("https://api.cas.nicovideo.jp/v1/services/live/programs/%s/watching-archive", opt.NicoLiveId) 76 | } else { 77 | uri = fmt.Sprintf("https://api.cas.nicovideo.jp/v1/services/live/programs/%s/watching", opt.NicoLiveId) 78 | } 79 | req, _ := http.NewRequest("POST", uri, data) 80 | 81 | //if opt.NicoSession != "" { 82 | req.Header.Set("Cookie", "user_session=" + opt.NicoSession) 83 | //} 84 | req.Header.Set("Accept", "application/json") 85 | req.Header.Set("Content-Type", "application/json") 86 | req.Header.Set("Origin", "https://cas.nicovideo.jp") 87 | req.Header.Set("X-Connection-Environment", "ethernet") 88 | req.Header.Set("X-Frontend-Id", "91") 89 | 90 | client := new(http.Client) 91 | resp, e := client.Do(req) 92 | if e != nil { 93 | err = e 94 | return 95 | } 96 | defer resp.Body.Close() 97 | bs, err := ioutil.ReadAll(resp.Body) 98 | if err != nil { 99 | log.Println(err) 100 | } 101 | 102 | if err = json.Unmarshal([]byte(bs), &props); err != nil { 103 | return 104 | } 105 | 106 | //obj.PrintAsJson(props) 107 | 108 | return 109 | } 110 | 111 | 112 | func jikkenPut(opt options.Option, actionTrackId string) (forbidden, notOnAir bool, err error) { 113 | str, _ := json.Marshal(OBJ{ 114 | "actionTrackId": actionTrackId, 115 | "isBroadcaster": false, 116 | }) 117 | if err != nil { 118 | log.Println(err) 119 | } 120 | fmt.Printf("\n%s\n\n", str) 121 | 122 | data := bytes.NewReader(str) 123 | 124 | uri := fmt.Sprintf("https://api.cas.nicovideo.jp/v1/services/live/programs/%s/watching", opt.NicoLiveId) 125 | req, _ := http.NewRequest("PUT", uri, data) 126 | 127 | //if opt.NicoSession != "" { 128 | req.Header.Set("Cookie", "user_session=" + opt.NicoSession) 129 | //} 130 | req.Header.Set("Accept", "application/json") 131 | req.Header.Set("Content-Type", "application/json") 132 | req.Header.Set("Origin", "https://cas.nicovideo.jp") 133 | req.Header.Set("X-Frontend-Id", "91") 134 | 135 | client := new(http.Client) 136 | resp, e := client.Do(req) 137 | if e != nil { 138 | err = e 139 | return 140 | } 141 | defer resp.Body.Close() 142 | bs, err := ioutil.ReadAll(resp.Body) 143 | if err != nil { 144 | log.Println(err) 145 | } 146 | 147 | var props interface{} 148 | if err = json.Unmarshal([]byte(bs), &props); err != nil { 149 | return 150 | } 151 | 152 | //obj.PrintAsJson(props) 153 | 154 | if code, ok := obj.FindString(props, "meta", "errorCode"); ok { 155 | switch code { 156 | case "FORBIDDEN": 157 | forbidden = true 158 | return 159 | case "PROGRAM_NOT_ONAIR": 160 | notOnAir = true 161 | return 162 | } 163 | } 164 | 165 | return 166 | } 167 | 168 | 169 | func jikkenHousou(nicoliveProgramId, title, userId, nickname, communityId string, opt options.Option, isArchive bool) (err error) { 170 | 171 | chInterrupt := make(chan os.Signal, 10) 172 | signal.Notify(chInterrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 173 | 174 | actionTrackId, err := getActionTrackId() 175 | if err != nil { 176 | log.Println(err) 177 | } 178 | 179 | media := &NicoMedia{} 180 | 181 | defer func() { 182 | if media.zipWriter != nil { 183 | media.zipWriter.Close() 184 | } 185 | }() 186 | 187 | title = files.ReplaceForbidden(title) 188 | nickname = files.ReplaceForbidden(nickname) 189 | media.fileName = fmt.Sprintf("%s-%s-%s.zip", nicoliveProgramId, nickname, title) 190 | 191 | 192 | var nLast int 193 | L_main: for { 194 | select { 195 | case <-chInterrupt: 196 | break L_main 197 | default: 198 | } 199 | props, e := jikkenWatching(opt, actionTrackId, isArchive) 200 | if e != nil { 201 | err = e 202 | log.Println(err) 203 | return 204 | } 205 | 206 | if uri, ok := obj.FindString(props, "data", "streamServer", "url"); ok { 207 | //fmt.Println(uri) 208 | 209 | is403, e := media.SetPlaylist(uri) 210 | if is403 { 211 | break L_main 212 | } 213 | if e != nil { 214 | err = e 215 | log.Println(e) 216 | return 217 | } 218 | } 219 | 220 | L_loc: for i := 0; true; i++ { 221 | select { 222 | case <-chInterrupt: 223 | break L_main 224 | default: 225 | } 226 | 227 | is403, e := media.GetMedias() 228 | if is403 { 229 | n := media.getNumChunk() 230 | if n != nLast { 231 | nLast = n 232 | break L_loc 233 | } else { 234 | break L_main 235 | } 236 | } 237 | if e != nil { 238 | log.Println(e) 239 | return 240 | } 241 | if i > 60 { 242 | forbidden, notOnAir, e := jikkenPut(opt, actionTrackId) 243 | if e != nil { 244 | err = e 245 | log.Println(e) 246 | return 247 | } 248 | if notOnAir { 249 | break L_main 250 | } 251 | if forbidden { 252 | break L_loc 253 | } 254 | i = 0 255 | } 256 | select { 257 | case <-chInterrupt: 258 | break L_main 259 | case <-time.After(1 * time.Second): 260 | } 261 | } 262 | } 263 | if media.zipWriter != nil { 264 | media.zipWriter.Close() 265 | } 266 | 267 | signal.Stop(chInterrupt) 268 | 269 | return 270 | } 271 | -------------------------------------------------------------------------------- /src/niconico/nico.go: -------------------------------------------------------------------------------- 1 | 2 | package niconico 3 | 4 | import ( 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "../options" 10 | "io/ioutil" 11 | "regexp" 12 | "strconv" 13 | "os" 14 | "net" 15 | "encoding/xml" 16 | "bufio" 17 | "os/signal" 18 | "syscall" 19 | "runtime" 20 | _ "net/http/pprof" 21 | "../httpbase" 22 | ) 23 | 24 | func NicoLogin(opt options.Option) (err error) { 25 | id, pass, _, _ := options.LoadNicoAccount(opt.NicoLoginAlias) 26 | 27 | if id == "" || pass == "" { 28 | err = fmt.Errorf("Login ID/Password not set. Use -nico-login \",\"") 29 | return 30 | } 31 | 32 | resp, err, neterr := httpbase.PostForm( 33 | "https://account.nicovideo.jp/api/v1/login", 34 | nil, 35 | url.Values{"mail_tel": {id}, "password": {pass}, "site": {"nicoaccountsdk"}}, 36 | ) 37 | if err != nil { 38 | return 39 | } 40 | if neterr != nil { 41 | err = neterr 42 | return 43 | } 44 | defer resp.Body.Close() 45 | 46 | body, err := ioutil.ReadAll(resp.Body) 47 | if err != nil { 48 | return 49 | } 50 | 51 | if ma := regexp.MustCompile(`(.+?)`).FindSubmatch(body); len(ma) > 0 { 52 | options.SetNicoSession(opt.NicoLoginAlias, string(ma[1])) 53 | 54 | fmt.Println("login success") 55 | } else { 56 | err = fmt.Errorf("login failed: session_key not found") 57 | return 58 | } 59 | return 60 | } 61 | 62 | func Record(opt options.Option) (hlsPlaylistEnd bool, dbName string, err error) { 63 | 64 | for i := 0; i < 2; i++ { 65 | // load session info 66 | if opt.NicoSession == "" || i > 0 { 67 | _, _, opt.NicoSession, _ = options.LoadNicoAccount(opt.NicoLoginAlias) 68 | } 69 | 70 | if (! opt.NicoRtmpOnly) { 71 | var done bool 72 | var notLogin bool 73 | var reserved bool 74 | done, hlsPlaylistEnd, notLogin, reserved, dbName, err = NicoRecHls(opt) 75 | if done { 76 | return 77 | } 78 | if err != nil { 79 | return 80 | } 81 | if notLogin { 82 | fmt.Println("not_login") 83 | if err = NicoLogin(opt); err != nil { 84 | return 85 | } 86 | continue 87 | } 88 | if reserved { 89 | continue 90 | } 91 | } 92 | 93 | if (! opt.NicoHlsOnly) { 94 | notLogin, e := NicoRecRtmp(opt) 95 | if e != nil { 96 | err = e 97 | return 98 | } 99 | if notLogin { 100 | fmt.Println("not_login") 101 | if err = NicoLogin(opt); err != nil { 102 | return 103 | } 104 | continue 105 | } 106 | } 107 | 108 | break 109 | } 110 | 111 | return 112 | } 113 | 114 | func TestRun(opt options.Option) (err error) { 115 | 116 | go func() { 117 | fmt.Println(http.ListenAndServe("localhost:6060", nil)) 118 | }() 119 | 120 | 121 | if false { 122 | ch := make(chan os.Signal, 10) 123 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 124 | go func() { 125 | <-ch 126 | os.Exit(0) 127 | }() 128 | } 129 | 130 | opt.NicoRtmpIndex = map[int]bool{ 131 | 0: true, 132 | } 133 | 134 | var nextId func() string 135 | 136 | if opt.NicoLiveId == "" { 137 | // niconama alert 138 | 139 | if opt.NicoTestTimeout <= 0 { 140 | opt.NicoTestTimeout = 12 141 | } 142 | 143 | 144 | resp, e, nete := httpbase.Get("http://live.nicovideo.jp/api/getalertinfo", nil) 145 | if e != nil { 146 | err = e 147 | return 148 | } 149 | if nete != nil { 150 | err = nete 151 | return 152 | } 153 | defer resp.Body.Close() 154 | 155 | switch resp.StatusCode { 156 | case 200: 157 | default: 158 | err = fmt.Errorf("StatusCode is %v", resp.StatusCode) 159 | return 160 | } 161 | 162 | type Alert struct { 163 | User string `xml:"user_id"` 164 | UserHash string `xml:"user_hash"` 165 | Addr string `xml:"ms>addr"` 166 | Port string `xml:"ms>port"` 167 | Thread string `xml:"ms>thread"` 168 | } 169 | status := &Alert{} 170 | dat, _ := ioutil.ReadAll(resp.Body) 171 | resp.Body.Close() 172 | 173 | err = xml.Unmarshal(dat, status) 174 | if err != nil { 175 | fmt.Println(string(dat)) 176 | fmt.Printf("error: %v", err) 177 | return 178 | } 179 | 180 | raddr, e := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%s", status.Addr, status.Port)) 181 | if e != nil { 182 | fmt.Printf("%v\n", e) 183 | return 184 | } 185 | 186 | conn, e := net.DialTCP("tcp", nil, raddr) 187 | if e != nil { 188 | err = e 189 | return 190 | } 191 | defer conn.Close() 192 | 193 | msg := fmt.Sprintf(`%c`, status.Thread, 0) 194 | if _, err = conn.Write([]byte(msg)); err != nil { 195 | fmt.Println(err) 196 | return 197 | } 198 | 199 | rdr := bufio.NewReader(conn) 200 | 201 | chLatest := make(chan string, 1000) 202 | go func(){ 203 | for { 204 | s, e := rdr.ReadString(0) 205 | if e != nil { 206 | fmt.Println(e) 207 | err = e 208 | return 209 | } 210 | //fmt.Println(s) 211 | if ma := regexp.MustCompile(`>(\d+),\S+,\S+<`).FindStringSubmatch(s); len(ma) > 0 { 212 | L0:for { 213 | select { 214 | case <-chLatest: 215 | default: 216 | break L0 217 | } 218 | } 219 | chLatest <- ma[1] 220 | } 221 | } 222 | }() 223 | 224 | nextId = func() (string) { 225 | L1:for { 226 | select { 227 | case <-chLatest: 228 | default: 229 | break L1 230 | } 231 | } 232 | return <-chLatest 233 | } 234 | 235 | } else { 236 | // start from NicoLiveId 237 | var id int64 238 | if ma := regexp.MustCompile(`\Alv(\d+)\z`).FindStringSubmatch(opt.NicoLiveId); len(ma) > 0 { 239 | if id, err = strconv.ParseInt(ma[1], 10, 64); err != nil { 240 | fmt.Println(err) 241 | return 242 | } 243 | } else { 244 | fmt.Println("TestRun: NicoLiveId not specified") 245 | return 246 | } 247 | 248 | nextId = func() (s string) { 249 | s = fmt.Sprintf("%d", id) 250 | id++ 251 | return 252 | } 253 | } 254 | 255 | if opt.NicoTestTimeout <= 0 { 256 | opt.NicoTestTimeout = 3 257 | } 258 | 259 | //chErr := make(chan error) 260 | var NFCount int 261 | var endCount int 262 | for { 263 | opt.NicoLiveId = fmt.Sprintf("lv%s", nextId()) 264 | 265 | fmt.Fprintf(os.Stderr, "start test: %s\n", opt.NicoLiveId) 266 | fmt.Fprintf(os.Stderr, "# NumGoroutine: %d\n", runtime.NumGoroutine()) 267 | 268 | var msg string 269 | _, _, err = Record(opt) 270 | if err != nil { 271 | if ma := regexp.MustCompile(`\AError\s+code:\s*(\S+)`).FindStringSubmatch(err.Error()); len(ma) > 0 { 272 | msg = ma[1] 273 | switch ma[1] { 274 | case "notfound", "closed", "comingsoon", "timeshift_ticket_exhaust": 275 | case "deletedbyuser", "deletedbyvisor", "violated": 276 | case "usertimeshift", "tsarchive", "require_community_member", 277 | "noauth", "full", "premium_only", "selected-country": 278 | default: 279 | fmt.Fprintf(os.Stderr, "unknown: %s\n", ma[1]) 280 | return 281 | } 282 | 283 | } else if strings.Contains(err.Error(), "closed network") { 284 | msg = "OK" 285 | } else { 286 | fmt.Fprintln(os.Stderr, err) 287 | return 288 | } 289 | } else { 290 | msg = "OK" 291 | } 292 | 293 | fmt.Fprintf(os.Stderr, "%s: %s\n---------\n", opt.NicoLiveId, msg) 294 | 295 | endCount++ 296 | if endCount > 100 { 297 | break 298 | } 299 | 300 | if msg == "notfound" { 301 | NFCount++ 302 | } else { 303 | NFCount = 0 304 | } 305 | if NFCount >= 10 { 306 | return 307 | } 308 | } 309 | return 310 | } 311 | -------------------------------------------------------------------------------- /src/niconico/nico_db.go: -------------------------------------------------------------------------------- 1 | package niconico 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "os" 7 | "log" 8 | "strings" 9 | "database/sql" 10 | 11 | "path/filepath" 12 | "../files" 13 | ) 14 | 15 | var SelMedia = `SELECT 16 | seqno, bandwidth, size, data FROM media 17 | WHERE IFNULL(notfound, 0) == 0 AND data IS NOT NULL 18 | ORDER BY seqno` 19 | 20 | var SelComment = `SELECT 21 | vpos, 22 | date, 23 | date_usec, 24 | IFNULL(no, -1) AS no, 25 | IFNULL(anonymity, 0) AS anonymity, 26 | user_id, 27 | content, 28 | IFNULL(mail, "") AS mail, 29 | %s 30 | IFNULL(premium, 0) AS premium, 31 | IFNULL(score, 0) AS score, 32 | thread, 33 | IFNULL(origin, "") AS origin, 34 | IFNULL(locale, "") AS locale 35 | FROM comment 36 | ORDER BY date2` 37 | 38 | func SelMediaF(seqnoStart, seqnoEnd int64) (ret string) { 39 | ret = `SELECT 40 | seqno, bandwidth, size, data FROM media 41 | WHERE IFNULL(notfound, 0) == 0 AND data IS NOT NULL` 42 | 43 | if seqnoStart > 0 { 44 | ret += ` AND seqno >= ` + fmt.Sprint(seqnoStart) 45 | } 46 | 47 | if seqnoEnd > 0 { 48 | ret += ` AND seqno <= ` + fmt.Sprint(seqnoEnd) 49 | } 50 | 51 | ret += ` ORDER BY seqno` 52 | 53 | return 54 | } 55 | 56 | func (hls *NicoHls) dbOpen() (err error) { 57 | db, err := sql.Open("sqlite3", hls.dbName) 58 | if err != nil { 59 | return 60 | } 61 | 62 | hls.db = db 63 | 64 | _, err = hls.db.Exec(` 65 | PRAGMA synchronous = OFF; 66 | PRAGMA journal_mode = WAL; 67 | `) 68 | if err != nil { 69 | return 70 | } 71 | 72 | err = hls.dbCreate() 73 | if err != nil { 74 | hls.db.Close() 75 | } 76 | return 77 | } 78 | 79 | func (hls *NicoHls) dbCreate() (err error) { 80 | hls.dbMtx.Lock() 81 | defer hls.dbMtx.Unlock() 82 | 83 | // table media 84 | 85 | _, err = hls.db.Exec(` 86 | CREATE TABLE IF NOT EXISTS media ( 87 | seqno INTEGER PRIMARY KEY NOT NULL UNIQUE, 88 | current INTEGER, 89 | position REAL, 90 | notfound INTEGER, 91 | bandwidth INTEGER, 92 | size INTEGER, 93 | data BLOB 94 | ) 95 | `) 96 | if err != nil { 97 | return 98 | } 99 | 100 | _, err = hls.db.Exec(` 101 | CREATE UNIQUE INDEX IF NOT EXISTS media0 ON media(seqno); 102 | CREATE INDEX IF NOT EXISTS media1 ON media(position); 103 | ---- for debug ---- 104 | CREATE INDEX IF NOT EXISTS media100 ON media(size); 105 | CREATE INDEX IF NOT EXISTS media101 ON media(notfound); 106 | `) 107 | if err != nil { 108 | return 109 | } 110 | 111 | // table comment 112 | 113 | _, err = hls.db.Exec(` 114 | CREATE TABLE IF NOT EXISTS comment ( 115 | vpos INTEGER NOT NULL, 116 | date INTEGER NOT NULL, 117 | date_usec INTEGER NOT NULL, 118 | date2 INTEGER NOT NULL, 119 | no INTEGER, 120 | anonymity INTEGER, 121 | user_id TEXT NOT NULL, 122 | content TEXT NOT NULL, 123 | mail TEXT, 124 | name TEXT, 125 | premium INTEGER, 126 | score INTEGER, 127 | thread TEXT, 128 | origin TEXT, 129 | locale TEXT, 130 | hash TEXT UNIQUE NOT NULL 131 | )`) 132 | if err != nil { 133 | return 134 | } 135 | 136 | _, err = hls.db.Exec(` 137 | CREATE UNIQUE INDEX IF NOT EXISTS comment0 ON comment(hash); 138 | ---- for debug ---- 139 | CREATE INDEX IF NOT EXISTS comment100 ON comment(date2); 140 | CREATE INDEX IF NOT EXISTS comment101 ON comment(no); 141 | `) 142 | if err != nil { 143 | return 144 | } 145 | 146 | 147 | // kvs media 148 | 149 | _, err = hls.db.Exec(` 150 | CREATE TABLE IF NOT EXISTS kvs ( 151 | k TEXT PRIMARY KEY NOT NULL UNIQUE, 152 | v BLOB 153 | ) 154 | `) 155 | if err != nil { 156 | return 157 | } 158 | _, err = hls.db.Exec(` 159 | CREATE UNIQUE INDEX IF NOT EXISTS kvs0 ON kvs(k); 160 | `) 161 | if err != nil { 162 | return 163 | } 164 | 165 | //hls.__dbBegin() 166 | 167 | return 168 | } 169 | 170 | // timeshift 171 | func (hls *NicoHls) dbSetPosition() { 172 | hls.dbExec(`UPDATE media SET position = ? WHERE seqno=?`, 173 | hls.playlist.position, 174 | hls.playlist.seqNo, 175 | ) 176 | } 177 | 178 | // timeshift 179 | func (hls *NicoHls) dbGetLastPosition() (res float64) { 180 | hls.dbMtx.Lock() 181 | defer hls.dbMtx.Unlock() 182 | 183 | hls.db.QueryRow("SELECT position FROM media ORDER BY POSITION DESC LIMIT 1").Scan(&res) 184 | return 185 | } 186 | 187 | //func (hls *NicoHls) __dbBegin() { 188 | // return 189 | /////////////////////////////////////////// 190 | //hls.db.Exec(`BEGIN TRANSACTION`) 191 | //} 192 | //func (hls *NicoHls) __dbCommit(t time.Time) { 193 | // return 194 | /////////////////////////////////////////// 195 | 196 | //// Never hls.dbMtx.Lock() 197 | //var start int64 198 | //hls.db.Exec(`COMMIT; BEGIN TRANSACTION`) 199 | //if t.UnixNano() - hls.lastCommit.UnixNano() > 500000000 { 200 | // log.Printf("Commit: %s\n", hls.dbName) 201 | //} 202 | //hls.lastCommit = t 203 | //} 204 | func (hls *NicoHls) dbCommit() { 205 | // hls.dbMtx.Lock() 206 | // defer hls.dbMtx.Unlock() 207 | 208 | // hls.__dbCommit(time.Now()) 209 | } 210 | func (hls *NicoHls) dbExec(query string, args ...interface{}) { 211 | hls.dbMtx.Lock() 212 | defer hls.dbMtx.Unlock() 213 | 214 | if hls.nicoDebug { 215 | start := time.Now().UnixNano() 216 | defer func() { 217 | t := (time.Now().UnixNano() - start) / (1000 * 1000) 218 | if t > 100 { 219 | fmt.Fprintf(os.Stderr, "%s:[WARN]dbExec: %d(ms):%s\n", debug_Now(), t, query) 220 | } 221 | }() 222 | } 223 | 224 | if _, err := hls.db.Exec(query, args...); err != nil { 225 | fmt.Printf("dbExec %#v\n", err) 226 | //hls.db.Exec("COMMIT") 227 | hls.db.Close() 228 | os.Exit(1) 229 | } 230 | } 231 | 232 | func (hls *NicoHls) dbKVSet(k string, v interface{}) { 233 | query := `INSERT OR REPLACE INTO kvs (k,v) VALUES (?,?)` 234 | hls.startDBGoroutine(func(sig <-chan struct{}) int { 235 | hls.dbExec(query, k, v) 236 | return OK 237 | }) 238 | } 239 | 240 | func (hls *NicoHls) dbInsertReplaceOrIgnore(table string, data map[string]interface{}, replace bool) { 241 | var keys []string 242 | var qs []string 243 | var args []interface{} 244 | 245 | for k, v := range data { 246 | keys = append(keys, k) 247 | qs = append(qs, "?") 248 | args = append(args, v) 249 | } 250 | 251 | var replaceOrIgnore string 252 | if replace { 253 | replaceOrIgnore = "REPLACE" 254 | } else { 255 | replaceOrIgnore = "IGNORE" 256 | } 257 | 258 | query := fmt.Sprintf( 259 | `INSERT OR %s INTO %s (%s) VALUES (%s)`, 260 | replaceOrIgnore, 261 | table, 262 | strings.Join(keys, ","), 263 | strings.Join(qs, ","), 264 | ) 265 | 266 | hls.startDBGoroutine(func(sig <-chan struct{}) int { 267 | hls.dbExec(query, args...) 268 | return OK 269 | }) 270 | } 271 | 272 | func (hls *NicoHls) dbInsert(table string, data map[string]interface{}) { 273 | hls.dbInsertReplaceOrIgnore(table, data, false) 274 | } 275 | func (hls *NicoHls) dbReplace(table string, data map[string]interface{}) { 276 | hls.dbInsertReplaceOrIgnore(table, data, true) 277 | } 278 | 279 | // timeshift 280 | func (hls *NicoHls) dbGetFromWhen() (res_from int, when float64) { 281 | hls.dbMtx.Lock() 282 | defer hls.dbMtx.Unlock() 283 | var date2 int64 284 | var no int 285 | 286 | hls.db.QueryRow("SELECT date2, no FROM comment ORDER BY date2 ASC LIMIT 1").Scan(&date2, &no) 287 | res_from = no 288 | if res_from <= 0 { 289 | res_from = 1 290 | } 291 | 292 | if date2 == 0 { 293 | var endTime float64 294 | hls.db.QueryRow(`SELECT v FROM kvs WHERE k = "endTime"`).Scan(&endTime) 295 | 296 | when = endTime + 3600 297 | } else { 298 | when = float64(date2) / (1000 * 1000) 299 | } 300 | 301 | return 302 | } 303 | 304 | func WriteComment(db *sql.DB, fileName string, skipHb bool) { 305 | 306 | var fSelComment = func(revision int) string { 307 | var selAppend string 308 | if revision >= 1 { 309 | selAppend += `IFNULL(name, "") AS name,` 310 | } 311 | return fmt.Sprintf(SelComment, selAppend) 312 | } 313 | 314 | var commentRevision int 315 | var nameCount int64 316 | db.QueryRow(`SELECT COUNT(name) FROM pragma_table_info('comment') WHERE name = 'name'`).Scan(&nameCount) 317 | if nameCount > 0 { 318 | commentRevision = 1 319 | } 320 | 321 | rows, err := db.Query(fSelComment(commentRevision)) 322 | if err != nil { 323 | log.Println(err) 324 | return 325 | } 326 | defer rows.Close() 327 | 328 | fileName = files.ChangeExtention(fileName, "xml") 329 | 330 | dir := filepath.Dir(fileName) 331 | base := filepath.Base(fileName) 332 | base, err = files.GetFileNameNext(base) 333 | if err != nil { 334 | fmt.Println(err) 335 | os.Exit(1) 336 | } 337 | fileName = filepath.Join(dir, base) 338 | f, err := os.Create(fileName) 339 | if err != nil { 340 | log.Fatalln(err) 341 | } 342 | defer f.Close() 343 | fmt.Fprintf(f, "%s\r\n", ``) 344 | fmt.Fprintf(f, "%s\r\n", ``) 345 | 346 | for rows.Next() { 347 | var vpos int64 348 | var date int64 349 | var date_usec int64 350 | var no int64 351 | var anonymity int64 352 | var user_id string 353 | var content string 354 | var mail string 355 | var name string 356 | var premium int64 357 | var score int64 358 | var thread string 359 | var origin string 360 | var locale string 361 | var dest0 = []interface{} { 362 | &vpos , 363 | &date , 364 | &date_usec , 365 | &no , 366 | &anonymity , 367 | &user_id , 368 | &content , 369 | &mail , 370 | } 371 | var dest1 = []interface{} { 372 | &premium , 373 | &score , 374 | &thread , 375 | &origin , 376 | &locale , 377 | } 378 | if commentRevision >= 1 { 379 | dest0 = append(dest0, &name) 380 | } 381 | var dest = append(dest0, dest1...) 382 | err = rows.Scan(dest...) 383 | if err != nil { 384 | log.Println(err) 385 | return 386 | } 387 | 388 | // skip /hb 389 | if (premium > 1) && skipHb && strings.HasPrefix(content, "/hb ") { 390 | continue 391 | } 392 | 393 | if (vpos < 0) { 394 | continue 395 | } 396 | 397 | line := fmt.Sprintf( 398 | `= 0 { 407 | line += fmt.Sprintf(` no="%d"`, no) 408 | } 409 | if anonymity != 0 { 410 | line += fmt.Sprintf(` anonymity="%d"`, anonymity) 411 | } 412 | if mail != "" { 413 | mail = strings.Replace(mail, `"`, """, -1) 414 | mail = strings.Replace(mail, "&", "&", -1) 415 | mail = strings.Replace(mail, "<", "<", -1) 416 | line += fmt.Sprintf(` mail="%s"`, mail) 417 | } 418 | if name != "" { 419 | name = strings.Replace(name, `"`, """, -1) 420 | name = strings.Replace(name, "&", "&", -1) 421 | name = strings.Replace(name, "<", "<", -1) 422 | line += fmt.Sprintf(` name="%s"`, name) 423 | } 424 | if origin != "" { 425 | origin = strings.Replace(origin, `"`, """, -1) 426 | origin = strings.Replace(origin, "&", "&", -1) 427 | origin = strings.Replace(origin, "<", "<", -1) 428 | line += fmt.Sprintf(` origin="%s"`, origin) 429 | } 430 | if premium != 0 { 431 | line += fmt.Sprintf(` premium="%d"`, premium) 432 | } 433 | if score != 0 { 434 | line += fmt.Sprintf(` score="%d"`, score) 435 | } 436 | if locale != "" { 437 | locale = strings.Replace(locale, `"`, """, -1) 438 | locale = strings.Replace(locale, "&", "&", -1) 439 | locale = strings.Replace(locale, "<", "<", -1) 440 | line += fmt.Sprintf(` locale="%s"`, locale) 441 | } 442 | line += ">" 443 | content = strings.Replace(content, "&", "&", -1) 444 | content = strings.Replace(content, "<", "<", -1) 445 | line += content 446 | line += "" 447 | fmt.Fprintf(f, "%s\r\n", line) 448 | } 449 | fmt.Fprintf(f, "%s\r\n", ``) 450 | } 451 | 452 | // ts 453 | func (hls *NicoHls) dbGetLastMedia(i int) (res []byte) { 454 | hls.dbMtx.Lock() 455 | defer hls.dbMtx.Unlock() 456 | hls.db.QueryRow("SELECT data FROM media WHERE seqno = ?", i).Scan(&res) 457 | return 458 | } 459 | func (hls *NicoHls) dbGetLastSeqNo() (res int64) { 460 | hls.dbMtx.Lock() 461 | defer hls.dbMtx.Unlock() 462 | hls.db.QueryRow("SELECT seqno FROM media ORDER BY seqno DESC LIMIT 1").Scan(&res) 463 | return 464 | } 465 | -------------------------------------------------------------------------------- /src/niconico/nico_mem_db.go: -------------------------------------------------------------------------------- 1 | package niconico 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "os" 7 | "database/sql" 8 | ) 9 | 10 | func (hls *NicoHls) memdbOpen() (err error) { 11 | db, err := sql.Open("sqlite3", "file::memory:?mode=memory&cache=shared") 12 | if err != nil { 13 | return 14 | } 15 | 16 | hls.memdb = db 17 | 18 | err = hls.memdbCreate() 19 | if err != nil { 20 | hls.memdb.Close() 21 | } 22 | 23 | if hls.db != nil { 24 | rows, e := hls.db.Query(`SELECT * FROM 25 | (SELECT seqno, IFNULL(notfound, 0), IFNULL(size, 0) FROM media ORDER BY seqno DESC LIMIT 10) ORDER BY seqno`) 26 | if e != nil { 27 | err = e 28 | return 29 | } 30 | defer rows.Close() 31 | 32 | var found404 bool 33 | for rows.Next() { 34 | var seqno int 35 | var notfound bool 36 | var size int 37 | err = rows.Scan(&seqno, ¬found, &size) 38 | if err != nil { 39 | return 40 | } 41 | if notfound || size == 0 { 42 | hls.memdbSet404(seqno) 43 | found404 = true 44 | } else { 45 | hls.memdbSet200(seqno) 46 | } 47 | if (! found404) { 48 | hls.memdbSetStopBack(seqno) 49 | if hls.nicoDebug { 50 | fmt.Fprintf(os.Stderr, "memdbSetStopBack(%d)\n", seqno) 51 | } 52 | } 53 | } 54 | } 55 | 56 | return 57 | } 58 | 59 | func (hls *NicoHls) memdbCreate() (err error) { 60 | hls.memdbMtx.Lock() 61 | defer hls.memdbMtx.Unlock() 62 | 63 | _, err = hls.memdb.Exec(` 64 | CREATE TABLE IF NOT EXISTS media ( 65 | seqno INTEGER PRIMARY KEY NOT NULL UNIQUE, 66 | is200 INTEGER, 67 | is404 INTEGER, 68 | stopback INTEGER 69 | ) 70 | `) 71 | if err != nil { 72 | return 73 | } 74 | 75 | _, err = hls.memdb.Exec(` 76 | CREATE UNIQUE INDEX IF NOT EXISTS media0 ON media(seqno); 77 | `) 78 | if err != nil { 79 | return 80 | } 81 | 82 | return 83 | } 84 | func (hls *NicoHls) memdbSetStopBack(seqno int) { 85 | if hls.nicoDebug { 86 | start := time.Now().UnixNano() 87 | defer func() { 88 | t := (time.Now().UnixNano() - start) / (1000 * 1000) 89 | if t > 100 { 90 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbSetStopBack: %d(ms)\n", debug_Now(), t) 91 | } 92 | }() 93 | } 94 | 95 | hls.memdbMtx.Lock() 96 | defer hls.memdbMtx.Unlock() 97 | 98 | _, err := hls.memdb.Exec(` 99 | INSERT OR IGNORE INTO media (seqno, stopback) VALUES (?, 1); 100 | UPDATE media SET stopback = 1 WHERE seqno=?; 101 | `, seqno, seqno) 102 | if err != nil { 103 | fmt.Println(err) 104 | } 105 | } 106 | func (hls *NicoHls) memdbGetStopBack(seqno int) (res bool) { 107 | if hls.nicoDebug { 108 | start := time.Now().UnixNano() 109 | defer func() { 110 | t := (time.Now().UnixNano() - start) / (1000 * 1000) 111 | if t > 100 { 112 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbGetStopBack: %d(ms)\n", debug_Now(), t) 113 | } 114 | }() 115 | } 116 | 117 | hls.memdbMtx.Lock() 118 | defer hls.memdbMtx.Unlock() 119 | 120 | hls.memdb.QueryRow("SELECT IFNULL(stopback, 0) FROM media WHERE seqno=?", seqno).Scan(&res) 121 | return 122 | } 123 | func (hls *NicoHls) memdbSet200(seqno int) { 124 | if hls.nicoDebug { 125 | start := time.Now().UnixNano() 126 | defer func() { 127 | t := (time.Now().UnixNano() - start) / (1000 * 1000) 128 | if t > 100 { 129 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbSet200: %d(ms)\n", debug_Now(), t) 130 | } 131 | }() 132 | } 133 | 134 | hls.memdbMtx.Lock() 135 | defer hls.memdbMtx.Unlock() 136 | 137 | hls.memdb.Exec(`INSERT OR REPLACE INTO media (seqno, is200) VALUES (?, 1)`, seqno) 138 | } 139 | func (hls *NicoHls) memdbSet404(seqno int) { 140 | if hls.nicoDebug { 141 | start := time.Now().UnixNano() 142 | defer func() { 143 | t := (time.Now().UnixNano() - start) / (1000 * 1000) 144 | if t > 100 { 145 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbSet404: %d(ms)\n", debug_Now(), t) 146 | } 147 | }() 148 | } 149 | 150 | hls.memdbMtx.Lock() 151 | defer hls.memdbMtx.Unlock() 152 | 153 | hls.memdb.Exec(`INSERT OR REPLACE INTO media (seqno, is404) VALUES (?, 1)`, seqno) 154 | } 155 | func (hls *NicoHls) memdbCheck200(seqno int) (res bool) { 156 | if hls.nicoDebug { 157 | start := time.Now().UnixNano() 158 | defer func() { 159 | t := (time.Now().UnixNano() - start) / (1000 * 1000) 160 | if t > 100 { 161 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbCheck200: %d(ms)\n", debug_Now(), t) 162 | } 163 | }() 164 | } 165 | 166 | hls.memdbMtx.Lock() 167 | defer hls.memdbMtx.Unlock() 168 | 169 | hls.memdb.QueryRow("SELECT IFNULL(is200, 0) FROM media WHERE seqno=?", seqno).Scan(&res) 170 | return 171 | } 172 | func (hls *NicoHls) memdbDelete(seqno int) { 173 | if hls.nicoDebug { 174 | start := time.Now().UnixNano() 175 | defer func() { 176 | t := (time.Now().UnixNano() - start) / (1000 * 1000) 177 | if t > 100 { 178 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbDelete: %d(ms)\n", debug_Now(), t) 179 | } 180 | }() 181 | } 182 | 183 | hls.memdbMtx.Lock() 184 | defer hls.memdbMtx.Unlock() 185 | 186 | min := seqno - 100 187 | hls.memdb.Exec(`DELETE FROM media WHERE seqno < ?`, min) 188 | } 189 | func (hls *NicoHls) memdbCount() (res int) { 190 | if hls.nicoDebug { 191 | start := time.Now().UnixNano() 192 | defer func() { 193 | t := (time.Now().UnixNano() - start) / (1000 * 1000) 194 | if t > 100 { 195 | fmt.Fprintf(os.Stderr, "%s:[WARN][MEMDB]memdbCount: %d(ms)\n", debug_Now(), t) 196 | } 197 | }() 198 | } 199 | 200 | hls.memdbMtx.Lock() 201 | defer hls.memdbMtx.Unlock() 202 | 203 | hls.memdb.QueryRow("SELECT COUNT(seqno) FROM media").Scan(&res) 204 | return 205 | } -------------------------------------------------------------------------------- /src/niconico/nico_rtmp.go: -------------------------------------------------------------------------------- 1 | package niconico 2 | 3 | import ( 4 | "fmt" 5 | "encoding/xml" 6 | "io/ioutil" 7 | "regexp" 8 | "strings" 9 | "net/url" 10 | "sync" 11 | "log" 12 | "time" 13 | "../rtmps" 14 | "../amf" 15 | "../options" 16 | "../files" 17 | "../httpbase" 18 | ) 19 | 20 | type Content struct { 21 | Id string `xml:"id,attr"` 22 | Text string `xml:",chardata"` 23 | } 24 | type Tickets struct { 25 | Name string `xml:"name,attr"` 26 | Text string `xml:",chardata"` 27 | } 28 | type Status struct { 29 | Title string `xml:"stream>title"` 30 | CommunityId string `xml:"stream>default_community"` 31 | Id string `xml:"stream>id"` 32 | Provider string `xml:"stream>provider_type"` 33 | IsArchive bool `xml:"stream>archive"` 34 | IsArchivePlayerServer bool `xml:"stream>is_archiveplayserver"` 35 | Ques []string `xml:"stream>quesheet>que"` 36 | Contents []Content `xml:"stream>contents_list>contents"` 37 | IsPremium bool `xml:"user>is_premium"` 38 | Url string `xml:"rtmp>url"` 39 | Ticket string `xml:"rtmp>ticket"` 40 | Tickets []Tickets `xml:"tickets>stream"` 41 | ErrorCode string `xml:"error>code"` 42 | streams []Stream 43 | chStream chan struct{} 44 | wg *sync.WaitGroup 45 | } 46 | type StatusE struct { 47 | Url string `xml:"rtmp>url"` 48 | Ticket string `xml:"rtmp>ticket"` 49 | ErrorCode string `xml:"error>code"` 50 | } 51 | type Stream struct { 52 | originUrl string 53 | streamName string 54 | originTicket string 55 | } 56 | func (status *Status) quesheet() { 57 | stream := make(map[string][]Stream) 58 | playType := make(map[string]string) 59 | 60 | // timeshift; tag 61 | re_pub := regexp.MustCompile(`\A/publish\s+(\S+)\s+(?:(\S+?),)?(\S+?)(?:\?(\S+))?\z`) 62 | re_play := regexp.MustCompile(`\A/play\s+(\S+)\s+(\S+)\z`) 63 | 64 | for _, q := range status.Ques { 65 | // /publish lv* /content/*/lv*_*_1_*.f4v 66 | if ma := re_pub.FindStringSubmatch(q); len(ma) >= 5 { 67 | stream[ma[1]] = append(stream[ma[1]], Stream{ 68 | originUrl: ma[2], 69 | streamName: ma[3], 70 | originTicket: ma[4], 71 | }) 72 | 73 | // /play ... 74 | } else if ma := re_play.FindStringSubmatch(q); len(ma) > 0 { 75 | // /play case:sp:rtmp:lv*_s_lv*,mobile:rtmp:lv*_s_lv*_sub1,premium:rtmp:lv*_s_lv*_sub1,default:rtmp:lv*_s_lv* main 76 | if strings.HasPrefix(ma[1], "case:") { 77 | s0 := ma[1] 78 | s0 = strings.TrimPrefix(s0, "case:") 79 | cases := strings.Split(s0, ",") 80 | // sp:rtmp:lv*_s_lv* 81 | re := regexp.MustCompile(`\A(\S+?):rtmp:(\S+?)\z`) 82 | for _, c := range cases { 83 | if ma := re.FindStringSubmatch(c); len(ma) > 0 { 84 | playType[ma[1]] = ma[2] 85 | } 86 | } 87 | 88 | // /play rtmp:lv* main 89 | } else { 90 | re := regexp.MustCompile(`\Artmp:(\S+?)\z`) 91 | if ma := re.FindStringSubmatch(ma[1]); len(ma) > 0 { 92 | playType["default"] = ma[1] 93 | } 94 | } 95 | } 96 | } 97 | 98 | pt, ok := playType["premium"] 99 | if ok && status.IsPremium { 100 | s, ok := stream[ pt ] 101 | if ok { 102 | status.streams = s 103 | } 104 | } else { 105 | pt, ok := playType["default"] 106 | if ok { 107 | s, ok := stream[ pt ] 108 | if ok { 109 | status.streams = s 110 | } 111 | } 112 | } 113 | } 114 | func (status *Status) initStreams() { 115 | 116 | if len(status.streams) > 0 { 117 | return 118 | } 119 | 120 | //if status.isOfficialLive() { 121 | status.contentsOfficialLive() 122 | //} else if status.isLive() { 123 | status.contentsNonOfficialLive() 124 | //} else { 125 | status.quesheet() 126 | //} 127 | 128 | return 129 | } 130 | func (status *Status) getFileName(index int) (name string) { 131 | if len(status.streams) == 1 { 132 | //name = fmt.Sprintf("%s.flv", status.Id) 133 | name = fmt.Sprintf("%s-%s-%s.flv", status.Id, status.CommunityId, status.Title) 134 | } else if len(status.streams) > 1 { 135 | //name = fmt.Sprintf("%s-%d.flv", status.Id, 1 + index) 136 | name = fmt.Sprintf("%s-%s-%s#%d.flv", status.Id, status.CommunityId, status.Title, 1 + index) 137 | } else { 138 | log.Fatalf("No stream") 139 | } 140 | name = files.ReplaceForbidden(name) 141 | return 142 | } 143 | func (status *Status) contentsNonOfficialLive() { 144 | re := regexp.MustCompile(`\A(?:rtmp:)?(rtmp\w*://\S+?)(?:,(\S+?)(?:\?(\S+))?)?\z`) 145 | 146 | // Live (not timeshift); tag 147 | for _, c := range status.Contents { 148 | if ma := re.FindStringSubmatch(c.Text); len(ma) > 0 { 149 | status.streams = append(status.streams, Stream{ 150 | originUrl: ma[1], 151 | streamName: ma[2], 152 | originTicket: ma[3], 153 | }) 154 | } 155 | } 156 | 157 | } 158 | func (status *Status) contentsOfficialLive() { 159 | 160 | tickets := make(map[string] string) 161 | for _, t := range status.Tickets { 162 | tickets[t.Name] = t.Text 163 | } 164 | 165 | for _, c := range status.Contents { 166 | if strings.HasPrefix(c.Text, "case:") { 167 | c.Text = strings.TrimPrefix(c.Text, "case:") 168 | 169 | for _, c := range strings.Split(c.Text, ",") { 170 | c, e := url.PathUnescape(c) 171 | if e != nil { 172 | fmt.Printf("%v\n", e) 173 | } 174 | 175 | re := regexp.MustCompile(`\A(\S+?):(?:limelight:|akamai:)?(\S+),(\S+)\z`) 176 | if ma := re.FindStringSubmatch(c); len(ma) > 0 { 177 | fmt.Printf("\n%#v\n", ma) 178 | switch ma[1] { 179 | default: 180 | fmt.Printf("unknown contents case %#v\n", ma[1]) 181 | case "mobile": 182 | case "middle": 183 | case "default": 184 | status.Url = ma[2] 185 | t, ok := tickets[ma[3]] 186 | if (! ok) { 187 | fmt.Printf("not found %s\n", ma[3]) 188 | } 189 | fmt.Printf("%s\n", t) 190 | status.streams = append(status.streams, Stream{ 191 | streamName: ma[3], 192 | originTicket: t, 193 | }) 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | func (status *Status) relayStreamName(i, offset int) (s string) { 202 | s = regexp.MustCompile(`[^/\\]+\z`).FindString(status.streams[i].streamName) 203 | if offset >= 0 { 204 | s += fmt.Sprintf("_%d", offset) 205 | } 206 | return 207 | } 208 | 209 | func (status *Status) streamName(i, offset int) (name string, err error) { 210 | if status.isOfficialLive() { 211 | if i >= len(status.streams) { 212 | err = fmt.Errorf("(status *Status) streamName(i int): Out of index: %d\n", i) 213 | return 214 | } 215 | 216 | name = status.streams[i].streamName 217 | if status.streams[i].originTicket != "" { 218 | name += "?" + status.streams[i].originTicket 219 | } 220 | return 221 | 222 | } else if status.isOfficialTs() { 223 | name = status.streams[i].streamName 224 | name = regexp.MustCompile(`(?i:\.flv)$`).ReplaceAllString(name, "") 225 | if regexp.MustCompile(`(?i:\.(?:f4v|mp4))$`).MatchString(name) { 226 | name = "mp4:" + name 227 | } else if regexp.MustCompile(`(?i:\.raw)$`).MatchString(name) { 228 | name = "raw:" + name 229 | } 230 | 231 | } else { 232 | name = status.relayStreamName(i, offset) 233 | } 234 | 235 | return 236 | } 237 | func (status *Status) tcUrl() (url string, err error) { 238 | if status.Url != "" { 239 | url = status.Url 240 | return 241 | } else { 242 | status.contentsOfficialLive() 243 | } 244 | 245 | if status.Url != "" { 246 | url = status.Url 247 | return 248 | } 249 | 250 | err = fmt.Errorf("tcUrl not found") 251 | return 252 | } 253 | func (status *Status) isTs() bool { 254 | return status.IsArchive 255 | } 256 | func (status *Status) isLive() bool { 257 | return (! status.IsArchive) 258 | } 259 | func (status *Status) isOfficialLive() bool { 260 | return (status.Provider == "official") && (! status.IsArchive) 261 | } 262 | func (status *Status) isOfficialTs() bool { 263 | if status.IsArchive { 264 | switch status.Provider { 265 | case "official": return true 266 | case "channel": return status.IsArchivePlayerServer 267 | } 268 | } 269 | return false 270 | } 271 | 272 | func (st Stream) relayStreamName(offset int) (s string) { 273 | s = regexp.MustCompile(`[^/\\]+\z`).FindString(st.streamName) 274 | if offset >= 0 { 275 | s += fmt.Sprintf("_%d", offset) 276 | } 277 | return 278 | } 279 | func (st Stream) noticeStreamName(offset int) (s string) { 280 | s = st.streamName 281 | s = regexp.MustCompile(`(?i:\.flv)$`).ReplaceAllString(s, "") 282 | if regexp.MustCompile(`(?i:\.(?:f4v|mp4))$`).MatchString(s) { 283 | s = "mp4:" + s 284 | } else if regexp.MustCompile(`(?i:\.raw)$`).MatchString(s) { 285 | s = "raw:" + s 286 | } 287 | 288 | if st.originTicket != "" { 289 | s += "?" + st.originTicket 290 | } 291 | 292 | return 293 | } 294 | 295 | func (status *Status) recStream(index int, opt options.Option) (err error) { 296 | defer func(){ 297 | <-status.chStream 298 | status.wg.Done() 299 | }() 300 | 301 | stream := status.streams[index] 302 | 303 | tcUrl, err := status.tcUrl() 304 | if err != nil { 305 | return 306 | } 307 | 308 | rtmp, err := rtmps.NewRtmp( 309 | // tcUrl 310 | tcUrl, 311 | // swfUrl 312 | "http://live.nicovideo.jp/nicoliveplayer.swf?180116154229", 313 | // pageUrl 314 | "http://live.nicovideo.jp/watch/" + status.Id, 315 | // option 316 | status.Ticket, 317 | ) 318 | if err != nil { 319 | return 320 | } 321 | defer rtmp.Close() 322 | 323 | 324 | fileName, err := files.GetFileNameNext(status.getFileName(index)) 325 | if err != nil { 326 | return 327 | } 328 | rtmp.SetFlvName(fileName) 329 | 330 | 331 | tryRecord := func() (incomplete bool, err error) { 332 | 333 | if err = rtmp.Connect(); err != nil { 334 | return 335 | } 336 | 337 | // default: 2500000 338 | //if err = rtmp.SetPeerBandwidth(100*1000*1000, 0); err != nil { 339 | if err = rtmp.SetPeerBandwidth(2500000, 0); err != nil { 340 | fmt.Printf("SetPeerBandwidth: %v\n", err) 341 | return 342 | } 343 | 344 | if err = rtmp.WindowAckSize(2500000); err != nil { 345 | fmt.Printf("WindowAckSize: %v\n", err) 346 | return 347 | } 348 | 349 | if err = rtmp.CreateStream(); err != nil { 350 | fmt.Printf("CreateStream %v\n", err) 351 | return 352 | } 353 | 354 | if err = rtmp.SetBufferLength(0, 2000); err != nil { 355 | fmt.Printf("SetBufferLength: %v\n", err) 356 | return 357 | } 358 | 359 | var offset int 360 | if status.IsArchive { 361 | offset = 0 362 | } else { 363 | offset = -2 364 | } 365 | 366 | if status.isOfficialTs() { 367 | for i := 0; true; i++ { 368 | if i > 30 { 369 | err = fmt.Errorf("sendFileRequest: No response") 370 | return 371 | } 372 | data, e := rtmp.Command( 373 | "sendFileRequest", []interface{} { 374 | nil, 375 | amf.SwitchToAmf3(), 376 | []string{ 377 | stream.streamName, 378 | }, 379 | }) 380 | if e != nil { 381 | err = e 382 | return 383 | } 384 | 385 | var resCnt int 386 | switch data.(type) { 387 | case map[string]interface{}: 388 | resCnt = len(data.(map[string]interface{})) 389 | case map[int]interface{}: 390 | resCnt = len(data.(map[int]interface{})) 391 | case []interface{}: 392 | resCnt = len(data.([]interface{})) 393 | case []string: 394 | resCnt = len(data.([]string)) 395 | } 396 | if resCnt > 0 { 397 | break 398 | } 399 | time.Sleep(10 * time.Second) 400 | } 401 | 402 | } else if (! status.isOfficialLive()) { 403 | // /publishの第二引数 404 | // streamName(param1:String) 405 | // 「,」で区切る 406 | // ._originUrl, streamName(playStreamName) 407 | // streamName に、「?」がついてるなら originTickt となる 408 | // streamName の.flvは削除する 409 | // streamNameが/\.(f4v|mp4)$/iなら、頭にmp4:をつける 410 | // /\.raw$/iなら、raw:をつける。 411 | // relayStreamName: streamNameの頭からスラッシュまでを削除したもの 412 | 413 | _, err = rtmp.Command( 414 | "nlPlayNotice", []interface{} { 415 | nil, 416 | // _connection.request.originUrl 417 | stream.originUrl, 418 | 419 | // this._connection.request.playStreamRequest 420 | // originticket あるなら 421 | // playStreamName ? this._originTicket 422 | // 無いなら playStreamName 423 | stream.noticeStreamName(offset), 424 | 425 | // var _loc1_:String = this._relayStreamName; 426 | // if(this._offset != -2) 427 | // { 428 | // _loc1_ = _loc1_ + ("_" + this.offset); 429 | // } 430 | // user nama: String 'lvxxxxxxxxx' 431 | // user kako: lvxxxxxxxxx_xxxxxxxxxxxx_1_xxxxxx.f4v_0 432 | stream.relayStreamName(offset), 433 | 434 | // seek offset 435 | // user nama: -2, user kako: 0 436 | offset, 437 | }) 438 | if err != nil { 439 | fmt.Printf("nlPlayNotice %v\n", err) 440 | return 441 | } 442 | } 443 | 444 | if err = rtmp.SetBufferLength(1, 3600 * 1000); err != nil { 445 | fmt.Printf("SetBufferLength: %v\n", err) 446 | return 447 | } 448 | 449 | // No return 450 | rtmp.SetFixAggrTimestamp(true) 451 | 452 | // user kako: lv*********_************_*_******.f4v_0 453 | // official or channel ts: mp4:/content/********/lv*********_************_*_******.f4v 454 | //if err = rtmp.Play(status.origin.playStreamName(status.isTsOfficial(), offset)); err != nil { 455 | streamName, err := status.streamName(index, offset) 456 | if err != nil { 457 | return 458 | } 459 | 460 | if status.isOfficialTs() { 461 | ts := rtmp.GetTimestamp() 462 | if ts > 1000 { 463 | err = rtmp.PlayTime(streamName, ts - 1000) 464 | } else { 465 | err = rtmp.PlayTime(streamName, -5000) 466 | } 467 | 468 | } else if status.isTs() { 469 | rtmp.SetFlush(true) 470 | err = rtmp.PlayTime(streamName, -5000) 471 | 472 | } else { 473 | err = rtmp.Play(streamName) 474 | } 475 | if err != nil { 476 | fmt.Printf("Play: %v\n", err) 477 | return 478 | } 479 | 480 | // Non-recordedなタイムシフトでseekしても、timestampが変わるだけで 481 | // 最初からの再生となってしまうのでやらないこと 482 | 483 | // 公式のタイムシフトでSeekしてもタイムスタンプがおかしい 484 | 485 | if opt.NicoTestTimeout > 0 { 486 | // test mode 487 | _, incomplete, err = rtmp.WaitTest(opt.NicoTestTimeout) 488 | } else { 489 | // normal mode 490 | _, incomplete, err = rtmp.Wait() 491 | } 492 | return 493 | } // end func 494 | 495 | //ticketTime := time.Now().Unix() 496 | //rtmp.SetNoSeek(false) 497 | for i := 0; i < 10; i++ { 498 | incomplete, e := tryRecord() 499 | if e != nil { 500 | err = e 501 | fmt.Printf("%v\n", e) 502 | return 503 | } else if incomplete && status.isOfficialTs() { 504 | fmt.Println("incomplete") 505 | time.Sleep(3 * time.Second) 506 | 507 | // update ticket 508 | if true { 509 | //if time.Now().Unix() > ticketTime + 60 { 510 | //ticketTime = time.Now().Unix() 511 | if ticket, e := getTicket(opt); e != nil { 512 | err = e 513 | return 514 | } else { 515 | rtmp.SetConnectOpt(ticket) 516 | } 517 | //} 518 | } 519 | 520 | continue 521 | } 522 | break 523 | } 524 | 525 | fmt.Printf("done\n") 526 | return 527 | } 528 | 529 | func (status *Status) recAllStreams(opt options.Option) (err error) { 530 | 531 | status.initStreams() 532 | 533 | var MaxConn int 534 | if opt.NicoRtmpMaxConn == 0 { 535 | if status.isOfficialTs() { 536 | MaxConn = 1 537 | } else { 538 | MaxConn = 4 539 | } 540 | } else if opt.NicoRtmpMaxConn < 0 { 541 | MaxConn = 1 542 | } else { 543 | MaxConn = opt.NicoRtmpMaxConn 544 | } 545 | 546 | status.wg = &sync.WaitGroup{} 547 | status.chStream = make(chan struct{}, MaxConn) 548 | 549 | ticketTime := time.Now().Unix() 550 | 551 | for index, _ := range status.streams { 552 | if opt.NicoRtmpIndex != nil { 553 | if tes, ok := opt.NicoRtmpIndex[index]; !ok || !tes { 554 | continue 555 | } 556 | } 557 | 558 | // blocks here 559 | status.chStream <- struct{}{} 560 | status.wg.Add(1) 561 | 562 | go status.recStream(index, opt) 563 | 564 | now := time.Now().Unix() 565 | if now > ticketTime + 60 { 566 | ticketTime = now 567 | if ticket, e := getTicket(opt); e != nil { 568 | err = e 569 | return 570 | } else { 571 | status.Ticket = ticket 572 | } 573 | } 574 | } 575 | 576 | status.wg.Wait() 577 | 578 | return 579 | } 580 | 581 | func getTicket(opt options.Option) (ticket string, err error) { 582 | status, notLogin, err := getStatus(opt) 583 | if err != nil { 584 | return 585 | } 586 | if status.Ticket != "" { 587 | ticket = status.Ticket 588 | } else { 589 | if notLogin { 590 | err = fmt.Errorf("notLogin") 591 | } else { 592 | err = fmt.Errorf("Ticket not found") 593 | } 594 | } 595 | return 596 | } 597 | func getStatus(opt options.Option) (status *Status, notLogin bool, err error) { 598 | var uri, uriE string 599 | 600 | // experimental 601 | if opt.NicoStatusHTTPS { 602 | uri = fmt.Sprintf("https://ow.live.nicovideo.jp/api/getplayerstatus?v=%s", opt.NicoLiveId) 603 | uriE = fmt.Sprintf("https://ow.live.nicovideo.jp/api/getedgestatus?v=%s", opt.NicoLiveId) 604 | } else { 605 | uri = fmt.Sprintf("http://watch.live.nicovideo.jp/api/getplayerstatus?v=%s", opt.NicoLiveId) 606 | uriE = fmt.Sprintf("http://watch.live.nicovideo.jp/api/getedgestatus?v=%s", opt.NicoLiveId) 607 | } 608 | 609 | header := make(map[string]string, 4) 610 | if opt.NicoSession != "" { 611 | header["Cookie"] = "user_session=" + opt.NicoSession 612 | } 613 | 614 | // experimental 615 | //if opt.NicoStatusHTTPS { 616 | // req.Header.Set("User-Agent", "Niconico/1.0 (Unix; U; iPhone OS 10.3.3; ja-jp; nicoiphone; iPhone5,2) Version/6.65") 617 | //} 618 | 619 | resp, err, neterr := httpbase.Get(uri, header) 620 | if err != nil { 621 | return 622 | } 623 | if neterr != nil { 624 | err = neterr 625 | return 626 | } 627 | defer resp.Body.Close() 628 | 629 | dat, _ := ioutil.ReadAll(resp.Body) 630 | status = &Status{} 631 | err = xml.Unmarshal(dat, status) 632 | if err != nil { 633 | //fmt.Println(string(dat)) 634 | fmt.Printf("error: %v", err) 635 | return 636 | } 637 | 638 | switch status.ErrorCode { 639 | case "": 640 | case "notlogin": 641 | notLogin = true 642 | default: 643 | err = fmt.Errorf("Error code: %s\n", status.ErrorCode) 644 | return 645 | } 646 | 647 | respE, err, neterr := httpbase.Get(uriE, header) 648 | if err != nil { 649 | return 650 | } 651 | if neterr != nil { 652 | err = neterr 653 | return 654 | } 655 | defer respE.Body.Close() 656 | 657 | datE, _ := ioutil.ReadAll(respE.Body) 658 | statusE := &StatusE{} 659 | err = xml.Unmarshal(datE, statusE) 660 | if err != nil { 661 | //fmt.Println(string(dat)) 662 | fmt.Printf("error: %v", err) 663 | return 664 | } 665 | 666 | switch statusE.ErrorCode { 667 | case "": 668 | case "notlogin": 669 | notLogin = true 670 | default: 671 | err = fmt.Errorf("Error code: %s\n", statusE.ErrorCode) 672 | return 673 | } 674 | 675 | if statusE.Url != "" { 676 | status.Url = statusE.Url 677 | } 678 | if statusE.Ticket != "" { 679 | status.Ticket = statusE.Ticket 680 | } 681 | 682 | return 683 | } 684 | 685 | func NicoRecRtmp(opt options.Option) (notLogin bool, err error) { 686 | status, notLogin, err := getStatus(opt) 687 | if err != nil { 688 | return 689 | } 690 | if notLogin { 691 | return 692 | } 693 | 694 | status.recAllStreams(opt) 695 | return 696 | } 697 | -------------------------------------------------------------------------------- /src/objs/objs.go: -------------------------------------------------------------------------------- 1 | 2 | package objs 3 | 4 | import ( 5 | "fmt" 6 | "encoding/json" 7 | ) 8 | 9 | func PrintAsJson(data interface{}) { 10 | json, err := json.MarshalIndent(data, "", " ") 11 | if err != nil { 12 | return 13 | } 14 | fmt.Println(string(json)) 15 | } 16 | func Find(intf interface{}, keylist... string) (res interface{}, ok bool) { 17 | res = intf 18 | if len(keylist) == 0 { 19 | ok = true 20 | return 21 | } 22 | for i, k := range keylist { 23 | var test bool 24 | //var obj map[string]interface{} 25 | switch res.(type) { 26 | case map[string]interface{}: 27 | res, test = res.(map[string]interface{})[k] 28 | if (! test) { 29 | ok = false 30 | return 31 | } 32 | case []interface{}: 33 | for _, o := range res.([]interface{}) { 34 | _res, _ok := Find(o, keylist[i:]...) 35 | if _ok { 36 | res = _res 37 | ok = _ok 38 | return 39 | } 40 | } 41 | } 42 | } 43 | ok = true 44 | return 45 | } 46 | func FindFloat64(intf interface{}, keylist... string) (res float64, ok bool) { 47 | val, ok := Find(intf, keylist...) 48 | if !ok { 49 | return 50 | } 51 | res, ok = val.(float64) 52 | return 53 | } 54 | func FindString(intf interface{}, keylist... string) (res string, ok bool) { 55 | val, ok := Find(intf, keylist...) 56 | if !ok { 57 | return 58 | } 59 | res, ok = val.(string) 60 | return 61 | } 62 | func FindBool(intf interface{}, keylist... string) (res bool, ok bool) { 63 | val, ok := Find(intf, keylist...) 64 | if !ok { 65 | return 66 | } 67 | res, ok = val.(bool) 68 | return 69 | } 70 | func FindArray(intf interface{}, keylist... string) (res []interface{}, ok bool) { 71 | val, ok := Find(intf, keylist...) 72 | if !ok { 73 | return 74 | } 75 | res, ok = val.([]interface{}) 76 | return 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/procs/base/base.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | func Open(cmdList *[]string, stdinEn, stdoutEn, stdErrEn, consoleEn bool, args []string) (cmd *exec.Cmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser, err error) { 10 | 11 | for i, cmdName := range *cmdList { 12 | cmd = exec.Command(cmdName, args...) 13 | 14 | if stdinEn { 15 | stdin, err = cmd.StdinPipe() 16 | if err != nil { 17 | return 18 | } 19 | } 20 | 21 | if stdoutEn { 22 | stdout, err = cmd.StdoutPipe() 23 | if err != nil { 24 | return 25 | } 26 | } else { 27 | if consoleEn { 28 | cmd.Stdout = os.Stdout 29 | } 30 | } 31 | 32 | if stdErrEn { 33 | stderr, err = cmd.StderrPipe() 34 | if err != nil { 35 | return 36 | } 37 | } else { 38 | if consoleEn { 39 | cmd.Stderr = os.Stderr 40 | } 41 | } 42 | 43 | if err = cmd.Start(); err != nil { 44 | continue 45 | } else { 46 | if i != 0 { 47 | *cmdList = []string{cmdName} 48 | } 49 | //fmt.Printf("CMD: %#v\n", cmd.Args) 50 | return 51 | } 52 | } 53 | 54 | // prog not found 55 | cmd = nil 56 | return 57 | } -------------------------------------------------------------------------------- /src/procs/ffmpeg/ffmpeg.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os/exec" 7 | "../base" 8 | ) 9 | 10 | var cmdList = []string{ 11 | "./bin/ffmpeg/bin/ffmpeg", 12 | "./bin/ffmpeg/ffmpeg", 13 | "./bin/ffmpeg", 14 | "./ffmpeg/bin/ffmpeg", 15 | "./ffmpeg/ffmpeg", 16 | "./ffmpeg", 17 | "ffmpeg", 18 | } 19 | 20 | func Open(opt... string) (cmd *exec.Cmd, stdin io.WriteCloser, err error) { 21 | cmd, stdin, _, _, err = base.Open(&cmdList, true, false, false, true, opt) 22 | if cmd == nil { 23 | err = fmt.Errorf("ffmpeg not found") 24 | return 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /src/procs/kill.go: -------------------------------------------------------------------------------- 1 | package procs 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "log" 7 | 8 | "./base" 9 | ) 10 | 11 | func Kill(pid int) { 12 | if runtime.GOOS == "windows" { 13 | options := []string{ 14 | "/PID", fmt.Sprintf("%v", pid), 15 | "/T", 16 | "/F", 17 | } 18 | list := []string{"taskkill"} 19 | if taskkill, _, _, _, err := base.Open(&list, false, false, false, false, options); err == nil { 20 | taskkill.Wait() 21 | } 22 | 23 | } else { 24 | log.Fatalf("[FIXME] Kill for %v not supported", runtime.GOOS) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/procs/streamlink/streamlink.go: -------------------------------------------------------------------------------- 1 | package streamlink 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os/exec" 7 | "../base" 8 | ) 9 | 10 | var cmdList = []string{ 11 | "./bin/streamlink/streamlink", 12 | "./bin/Streamlink/Streamlink", 13 | "./bin/streamlink", 14 | "./bin/Streamlink", 15 | "./streamlink/streamlink", 16 | "./Streamlink/Streamlink", 17 | "./Streamlink", 18 | "streamlink", 19 | "Streamlink", 20 | } 21 | 22 | func Open(opt... string) (cmd *exec.Cmd, stdout, stderr io.ReadCloser, err error) { 23 | cmd, _, stdout, stderr, err = base.Open(&cmdList, false, true, true, false, opt) 24 | if cmd == nil { 25 | err = fmt.Errorf("streamlink not found") 26 | return 27 | } 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /src/procs/youtube_dl/youtube-dl.go: -------------------------------------------------------------------------------- 1 | package youtube_dl 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os/exec" 7 | "../base" 8 | ) 9 | 10 | var cmdList = []string{ 11 | "./bin/youtube-dl/youtube-dl", 12 | "./bin/youtube-dl", 13 | "./youtube-dl/youtube-dl", 14 | "./youtube-dl", 15 | "youtube-dl", 16 | } 17 | 18 | func Open(opt... string) (cmd *exec.Cmd, stdout, stderr io.ReadCloser, err error) { 19 | cmd, _, stdout, stderr, err = base.Open(&cmdList, false, true, true, false, opt) 20 | if cmd == nil { 21 | err = fmt.Errorf("youtube-dl not found") 22 | return 23 | } 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /src/rtmps/message.go: -------------------------------------------------------------------------------- 1 | package rtmps 2 | 3 | import ( 4 | "encoding/binary" 5 | "time" 6 | "bytes" 7 | "log" 8 | "io" 9 | 10 | "fmt" 11 | "../amf" 12 | ) 13 | 14 | const ( 15 | TID_SETCHUNKSIZE = 1 16 | TID_ABORT = 2 17 | TID_ACKNOWLEDGEMENT = 3 18 | TID_USERCONTROL = 4 19 | TID_WINDOW_ACK_SIZE = 5 20 | TID_SETPEERBANDWIDTH = 6 21 | TID_AUDIO = 8 22 | TID_VIDEO = 9 23 | TID_AMF3COMMAND = 17 24 | TID_AMF0COMMAND = 20 25 | TID_AMF0DATA = 18 26 | TID_AMF3DATA = 15 27 | TID_AGGREGATE = 22 28 | ) 29 | 30 | const ( 31 | UC_STREAMBEGIN = 0 32 | UC_STREAMEOF = 1 33 | UC_STREAMDRY = 2 34 | UC_SETBUFFERLENGTH = 3 35 | UC_STREAMISRECORDED = 4 36 | UC_PINGREQUEST = 6 37 | UC_PINGRESPONSE = 7 38 | 39 | UC_BUFFEREMPTY = 31 40 | UC_BUFFERREADY = 32 41 | ) 42 | 43 | 44 | func intToBE16(num int) (data []byte) { 45 | tmp := make([]byte, 2) 46 | binary.BigEndian.PutUint16(tmp, uint16(num)) 47 | data = append(data, tmp[:]...) 48 | return 49 | } 50 | func intToBE24(num int) (data []byte) { 51 | tmp := make([]byte, 4) 52 | binary.BigEndian.PutUint32(tmp, uint32(num)) 53 | data = append(data, tmp[1:]...) 54 | return 55 | } 56 | func intToBE32(num int) (data []byte) { 57 | tmp := make([]byte, 4) 58 | binary.BigEndian.PutUint32(tmp, uint32(num)) 59 | data = append(data, tmp[:]...) 60 | return 61 | } 62 | func intToLE32(num int) (data []byte) { 63 | tmp := make([]byte, 4) 64 | binary.LittleEndian.PutUint32(tmp, uint32(num)) 65 | data = append(data, tmp[:]...) 66 | return 67 | } 68 | 69 | 70 | func chunkBasicHeader(fmt, csid int) (data []byte) { 71 | if 2 <= csid && csid <= 63 { 72 | b := byte(((fmt & 3) << 6) | (csid & 0x3F)) 73 | data = append(data, b) 74 | 75 | } else if 64 <= csid && csid <= 319 { 76 | b0 := byte((fmt & 3) << 6) 77 | b1 := byte(csid - 64) 78 | data = append(data, b0, b1) 79 | 80 | } else if 320 <= csid && csid <= 65599 { 81 | b0 := byte(((fmt & 3) << 6) | 1) 82 | b1 := byte((csid & 0xFF) - 64) 83 | b2 := byte(csid >> 8) 84 | data = append(data, b0, b1, b2) 85 | 86 | } else { 87 | log.Printf("[FIXME] Chunk basic header: csid out of range: %d", csid) 88 | } 89 | 90 | return 91 | } 92 | 93 | var start = millisec() 94 | 95 | func millisec() int64 { 96 | return time.Now().UnixNano() / int64(time.Millisecond) 97 | } 98 | func getTime() int { 99 | delta := millisec() - start 100 | return int(delta) 101 | } 102 | 103 | func type0(buff *bytes.Buffer, csId int, typeId byte, streamId int, length int) { 104 | buff.Write(chunkBasicHeader(0, csId)) 105 | 106 | // timestamp 107 | //buff.Write(intToBE24(getTime())) 108 | buff.Write(intToBE24(0)) 109 | // message length 110 | buff.Write(intToBE24(length)) 111 | // message type id 112 | buff.WriteByte(typeId) 113 | // Stream ID 114 | buff.Write(intToLE32(streamId)) 115 | // body 116 | //buff.Write(body) 117 | 118 | return 119 | } 120 | func type3(buff *bytes.Buffer, csId int) { 121 | buff.Write(chunkBasicHeader(3, csId)) 122 | } 123 | func encodeAcknowledgement(asz int) (buff *bytes.Buffer, err error){ 124 | buff = bytes.NewBuffer(nil) 125 | bsz := intToBE32(asz) 126 | type0(buff, 2, TID_ACKNOWLEDGEMENT, 0, len(bsz)) 127 | if _, err = buff.Write(bsz); err != nil { 128 | return 129 | } 130 | return 131 | } 132 | func encodeWindowAckSize(asz int) (buff *bytes.Buffer, err error){ 133 | buff = bytes.NewBuffer(nil) 134 | bsz := intToBE32(asz) 135 | type0(buff, 2, TID_WINDOW_ACK_SIZE, 0, len(bsz)) 136 | if _, err = buff.Write(bsz); err != nil { 137 | return 138 | } 139 | return 140 | } 141 | func encodeSetPeerBandwidth(wsz, lim int) (buff *bytes.Buffer, err error){ 142 | buff = bytes.NewBuffer(nil) 143 | b := intToBE32(wsz) 144 | b = append(b, byte(lim)) 145 | type0(buff, 2, TID_SETPEERBANDWIDTH, 0, len(b)) 146 | if _, err = buff.Write(b); err != nil { 147 | return 148 | } 149 | return 150 | } 151 | 152 | func encodePingResponse(timestamp int) (buff *bytes.Buffer, err error){ 153 | buff = bytes.NewBuffer(nil) 154 | 155 | var body []byte 156 | body = append(body, intToBE16(UC_PINGRESPONSE)...) 157 | body = append(body, intToBE32(timestamp)...) 158 | 159 | type0(buff, 2, TID_USERCONTROL, 0, len(body)) 160 | if _, err = buff.Write(body); err != nil { 161 | return 162 | } 163 | return 164 | } 165 | func encodeSetBufferLength(streamId, length int) (buff *bytes.Buffer, err error){ 166 | buff = bytes.NewBuffer(nil) 167 | 168 | var body []byte 169 | body = append(body, intToBE16(UC_SETBUFFERLENGTH)...) 170 | body = append(body, intToBE32(streamId)...) 171 | body = append(body, intToBE32(int(length))...) 172 | 173 | type0(buff, 2, TID_USERCONTROL, 0, len(body)) 174 | if _, err = buff.Write(body); err != nil { 175 | return 176 | } 177 | return 178 | } 179 | func amf0Command(chunkSize, csId, streamId int, body []byte) (wbuff *bytes.Buffer, err error) { 180 | wbuff = bytes.NewBuffer(nil) 181 | rbuff := bytes.NewBuffer(body) 182 | 183 | type0(wbuff, csId, TID_AMF0COMMAND, streamId, rbuff.Len()) 184 | if chunkSize < rbuff.Len() { 185 | if _, err = io.CopyN(wbuff, rbuff, int64(chunkSize)); err != nil { 186 | return 187 | } 188 | } else { 189 | if _, err = io.CopyN(wbuff, rbuff, int64(rbuff.Len())); err != nil { 190 | return 191 | } 192 | } 193 | 194 | for rbuff.Len() > 0 { 195 | type3(wbuff, csId) 196 | 197 | if chunkSize < rbuff.Len() { 198 | if _, err = io.CopyN(wbuff, rbuff, int64(chunkSize)); err != nil { 199 | return 200 | } 201 | } else { 202 | if _, err = io.CopyN(wbuff, rbuff, int64(rbuff.Len())); err != nil { 203 | return 204 | } 205 | } 206 | } 207 | 208 | //log.Fatalf("amf0Command %#v", wbuff) 209 | 210 | return 211 | } 212 | 213 | 214 | 215 | func decodeFmtCsId(rdr io.Reader, msg *rtmpMsg) (err error) { 216 | b0 := make([]byte, 1) 217 | msg.hdrLength++ 218 | _, err = io.ReadFull(rdr, b0); if err != nil { 219 | return 220 | } 221 | format := (int(b0[0]) >> 6) & 3 222 | csId := int(b0[0]) & 0x3F 223 | switch csId { 224 | case 0: 225 | b1 := make([]byte, 1) 226 | msg.hdrLength++ 227 | if _, err = io.ReadFull(rdr, b1); err != nil { 228 | return 229 | } 230 | csId = int(b1[0]) + 64 231 | 232 | case 1: 233 | b1 := make([]byte, 2) 234 | msg.hdrLength += 2 235 | if _, err = io.ReadFull(rdr, b1); err != nil { 236 | return 237 | } 238 | csId = (int(b1[1]) << 8) | (int(b1[0]) + 64) 239 | } 240 | 241 | msg.format = format 242 | msg.csId = csId 243 | if (! msg.readingBody) { 244 | msg.formatOrigin = format 245 | msg.csIdOrigin = csId 246 | } 247 | // fmt.Printf("debug format type %v csid %v\n", format, csId) 248 | return 249 | } 250 | 251 | func decodeInt8(rdr io.Reader) (num int, err error) { 252 | buf := make([]byte, 1) 253 | if _, err = io.ReadFull(rdr, buf); err != nil { 254 | return 255 | } 256 | num = int(buf[0]) 257 | return 258 | } 259 | func decodeBEInt16(rdr io.Reader) (num int, err error) { 260 | buf := make([]byte, 2) 261 | if _, err = io.ReadFull(rdr, buf); err != nil { 262 | return 263 | } 264 | num = (int(buf[0]) << 8) | int(buf[1]) 265 | return 266 | } 267 | func decodeBEInt24(rdr io.Reader) (num int, err error) { 268 | buf := make([]byte, 3) 269 | if _, err = io.ReadFull(rdr, buf); err != nil { 270 | return 271 | } 272 | num = (int(buf[0]) << 16) | (int(buf[1]) << 8) | int(buf[2]) 273 | return 274 | } 275 | func decodeBEInt32(rdr io.Reader) (num int, err error) { 276 | buf := make([]byte, 4) 277 | if _, err = io.ReadFull(rdr, buf); err != nil { 278 | return 279 | } 280 | num = (int(buf[0]) << 24) | (int(buf[1]) << 16) | (int(buf[2]) << 8) | int(buf[3]) 281 | return 282 | } 283 | func decodeLEInt32(rdr io.Reader) (num int, err error) { 284 | buf := make([]byte, 4) 285 | if _, err = io.ReadFull(rdr, buf); err != nil { 286 | return 287 | } 288 | num = (int(buf[3]) << 24) | (int(buf[2]) << 16) | (int(buf[1]) << 8) | int(buf[0]) 289 | return 290 | } 291 | 292 | func decodeTimestamp(rdr io.Reader, msg *rtmpMsg) (err error) { 293 | msg.hdrLength += 3 294 | timestamp, err := decodeBEInt24(rdr) 295 | if err != nil { 296 | return 297 | } 298 | msg.timestampField = timestamp 299 | 300 | return 301 | } 302 | func decodeTimestampEX(rdr io.Reader, msg *rtmpMsg) (err error) { 303 | msg.hdrLength += 4 304 | timestamp, err := decodeBEInt32(rdr) 305 | if err != nil { 306 | return 307 | } 308 | //fmt.Printf("decodeTimestampEX %v\n", timestamp) 309 | if (! msg.readingBody) { 310 | msg.timestampEx = timestamp 311 | } 312 | 313 | return 314 | } 315 | func decodeMsgLength(rdr io.Reader, msg *rtmpMsg) (err error) { 316 | msg.hdrLength += 3 317 | length, err := decodeBEInt24(rdr) 318 | msg.msgLength = length 319 | return 320 | } 321 | func decodeMsgType(rdr io.Reader, msg *rtmpMsg) (err error) { 322 | msg.hdrLength += 1 323 | msg_t, err := decodeInt8(rdr) 324 | msg.msgTypeId = msg_t 325 | return 326 | } 327 | func decodeStreamId(rdr io.Reader, msg *rtmpMsg) (err error) { 328 | msg.hdrLength += 4 329 | sid, err := decodeLEInt32(rdr) 330 | msg.msgStreamId = sid 331 | return 332 | } 333 | 334 | func decodeType0(rdr io.Reader, msg *rtmpMsg) (err error) { 335 | if err = decodeTimestamp(rdr, msg); err != nil { 336 | return 337 | } 338 | if err = decodeMsgLength(rdr, msg); err != nil { 339 | return 340 | } 341 | if err = decodeMsgType(rdr, msg); err != nil { 342 | return 343 | } 344 | err = decodeStreamId(rdr, msg) 345 | return 346 | } 347 | func decodeType1(rdr io.Reader, msg *rtmpMsg) (err error) { 348 | if err = decodeTimestamp(rdr, msg); err != nil { 349 | return 350 | } 351 | if err = decodeMsgLength(rdr, msg); err != nil { 352 | return 353 | } 354 | err = decodeMsgType(rdr, msg) 355 | return 356 | } 357 | func decodeType2(rdr io.Reader, msg *rtmpMsg) (err error) { 358 | err = decodeTimestamp(rdr, msg) 359 | return 360 | } 361 | 362 | 363 | 364 | 365 | type rtmpMsg struct { 366 | format int 367 | formatOrigin int 368 | csId int 369 | csIdOrigin int 370 | timestampField int 371 | timestampDelta int 372 | timestampEx int 373 | timestampActual int 374 | msgLength int 375 | msgTypeId int 376 | msgStreamId int 377 | bodyBuff *bytes.Buffer 378 | 379 | readingBody bool 380 | hdrLength int 381 | splitCount int 382 | } 383 | 384 | func readChunkBody(rdr io.Reader, msg *rtmpMsg, csz int) (err error) { 385 | 386 | if msg.bodyBuff == nil { 387 | msg.bodyBuff = bytes.NewBuffer(nil) 388 | } 389 | rem := msg.msgLength - msg.bodyBuff.Len() 390 | //fmt.Printf("readChunkBody: %v %v\n", msg.msgLength, msg.bodyBuff.Len()) 391 | if rem > csz { 392 | _, err = io.CopyN(msg.bodyBuff, rdr, int64(csz)) 393 | } else { 394 | _, err = io.CopyN(msg.bodyBuff, rdr, int64(rem)) 395 | } 396 | if err != nil { 397 | return 398 | } 399 | 400 | return 401 | } 402 | 403 | func decodeHeader(rdr io.Reader, msg *rtmpMsg) (err error) { 404 | if err = decodeFmtCsId(rdr, msg); err != nil { 405 | return 406 | } 407 | switch msg.format { 408 | case 0: 409 | if err = decodeType0(rdr, msg); err != nil { 410 | return 411 | } 412 | case 1: 413 | if err = decodeType1(rdr, msg); err != nil { 414 | return 415 | } 416 | case 2: 417 | if err = decodeType2(rdr, msg); err != nil { 418 | return 419 | } 420 | case 3: 421 | if (msg.readingBody) { 422 | msg.splitCount++ 423 | if msg.csId != msg.csIdOrigin { 424 | err = &DecodeError{ 425 | Fun: "decodeHeader", 426 | Msg: fmt.Sprintf("msg.csId(%d) != msg.csIdOrigin(%d)", msg.csId, msg.csIdOrigin), 427 | } 428 | return 429 | } 430 | } 431 | default: 432 | err = &DecodeError{ 433 | Fun: "decodeHeader", 434 | Msg: fmt.Sprintf("Unknown fmt: %v", msg.format), 435 | } 436 | return 437 | } 438 | 439 | return 440 | } 441 | 442 | func decodeSetChunkSize(rbuff *bytes.Buffer) (csz int, err error) { 443 | num, e := decodeBEInt32(rbuff) 444 | if e != nil { 445 | err = e 446 | return 447 | } 448 | csz = num & 0x7fffffff 449 | return 450 | } 451 | 452 | func decodeWindowAckSize(rbuff *bytes.Buffer) (asz int, err error) { 453 | asz, e := decodeBEInt32(rbuff) 454 | if e != nil { 455 | err = e 456 | return 457 | } 458 | return 459 | } 460 | 461 | func decodeSetPeerBandwidth(rbuff *bytes.Buffer) (res []int, err error) { 462 | wsz, err := decodeBEInt32(rbuff) 463 | if err != nil { 464 | return 465 | } 466 | lim, err := decodeInt8(rbuff) 467 | if err != nil { 468 | return 469 | } 470 | res = append(res, wsz, lim) 471 | return 472 | } 473 | func decodeUserControl(rbuff *bytes.Buffer) (res []int, err error) { 474 | evt, err := decodeBEInt16(rbuff) 475 | if err != nil { 476 | return 477 | } 478 | res = append(res, evt) 479 | switch evt { 480 | case UC_BUFFEREMPTY, UC_BUFFERREADY: // Buffer Empty, Buffer Ready 481 | // http://repo.or.cz/w/rtmpdump.git/blob/8880d1456b282ee79979adbe7b6a6eb8ad371081:/librtmp/rtmp.c#l2787 482 | 483 | case 484 | UC_STREAMBEGIN, 485 | UC_STREAMEOF, 486 | UC_STREAMDRY, 487 | UC_STREAMISRECORDED, 488 | UC_PINGREQUEST, 489 | UC_PINGRESPONSE: 490 | // 4-byte stream id 491 | num, e := decodeBEInt32(rbuff) 492 | if e != nil { 493 | err = e 494 | return 495 | } 496 | res = append(res, num) 497 | 498 | case UC_SETBUFFERLENGTH: 499 | // 4-byte stream id 500 | sid, e := decodeBEInt32(rbuff) 501 | if e != nil { 502 | err = e 503 | return 504 | } 505 | res = append(res, sid) 506 | // 4-byte buffer length 507 | bsz, e := decodeBEInt32(rbuff) 508 | if e != nil { 509 | err = e 510 | return 511 | } 512 | res = append(res, bsz) 513 | 514 | default: 515 | err = &DecodeError{ 516 | Fun: "decodeUserControl", 517 | Msg: fmt.Sprintf("Unknown User control: %v", evt), 518 | } 519 | return 520 | } 521 | return 522 | } 523 | 524 | type message struct { 525 | msg_t int 526 | timestamp int 527 | data *bytes.Buffer 528 | } 529 | func decodeMessage(rbuff *bytes.Buffer) (res message, err error) { 530 | msg_t, err := decodeInt8(rbuff) 531 | if err != nil { 532 | return 533 | } 534 | plen, err := decodeBEInt24(rbuff) 535 | if err != nil { 536 | return 537 | } 538 | ts_0, err := decodeBEInt24(rbuff) 539 | if err != nil { 540 | return 541 | } 542 | ts_1, err := decodeInt8(rbuff) 543 | if err != nil { 544 | return 545 | } 546 | ts := (ts_1 << 24) | ts_0 547 | 548 | // stream id 549 | _, err = decodeBEInt24(rbuff) 550 | if err != nil { 551 | return 552 | } 553 | //fmt.Printf("debug decodeMessage: type(%v) len(%v) ts(%v)\n", msg_t, plen, ts_0) 554 | buff := bytes.NewBuffer(nil) 555 | if _, err = io.CopyN(buff, rbuff, int64(plen)); err != nil { 556 | return 557 | } 558 | 559 | // backPointer 560 | _, err = decodeBEInt32(rbuff) 561 | if err != nil { 562 | return 563 | } 564 | 565 | res = message{ 566 | msg_t: msg_t, 567 | timestamp: ts, 568 | data: buff, 569 | } 570 | 571 | return 572 | } 573 | func decodeAggregate(rbuff *bytes.Buffer) (res []message, err error) { 574 | for rbuff.Len() > 0 { 575 | msg, e := decodeMessage(rbuff) 576 | if e != nil { 577 | err = e 578 | return 579 | } 580 | res = append(res, msg) 581 | } 582 | return 583 | } 584 | 585 | func decodeOne(rdr io.Reader, csz int, info map[int] chunkInfo) (ts int, msg_t int, res interface{}, rsz int, err error) { 586 | msg := rtmpMsg{} 587 | 588 | // rtmp header 589 | if err = decodeHeader(rdr, &msg); err != nil { 590 | return 591 | } 592 | 593 | // restore fields from previous chunk header 594 | 595 | var prevChunk chunkInfo 596 | if msg.formatOrigin != 0 { 597 | var ok bool 598 | if prevChunk, ok = info[msg.csIdOrigin]; (! ok) { 599 | err = &DecodeError{ 600 | Fun: "decodeOne", 601 | Msg: fmt.Sprintf("Not exists previous chunk(csId = %v)", msg.csIdOrigin), 602 | } 603 | return 604 | } 605 | } 606 | //fmt.Printf("debug decodeOne msg.timestampField %d\n", msg.timestampField) 607 | if (msg.timestampField == 0xffffff) || ((msg.formatOrigin == 3) && (prevChunk.timestampField == 0xffffff)) { 608 | if err = decodeTimestampEX(rdr, &msg); err != nil { 609 | return 610 | } 611 | //fmt.Printf("%#v\n", msg) 612 | switch msg.formatOrigin { 613 | case 0: 614 | msg.timestampActual = msg.timestampEx 615 | msg.timestampDelta = msg.timestampEx 616 | case 1, 2: 617 | msg.timestampActual = prevChunk.timestampActual + msg.timestampEx 618 | msg.timestampDelta = msg.timestampEx 619 | case 3: 620 | msg.timestampActual = msg.timestampEx 621 | msg.timestampDelta = msg.timestampEx 622 | msg.timestampField = 0xffffff 623 | } 624 | } else { 625 | switch msg.formatOrigin { 626 | case 0: 627 | msg.timestampActual = msg.timestampField 628 | msg.timestampDelta = msg.timestampField 629 | case 1, 2: 630 | msg.timestampActual = prevChunk.timestampActual + msg.timestampField 631 | msg.timestampDelta = msg.timestampField 632 | case 3: 633 | msg.timestampActual = prevChunk.timestampActual + prevChunk.timestampDelta 634 | msg.timestampDelta = prevChunk.timestampDelta 635 | } 636 | } 637 | 638 | switch msg.formatOrigin { 639 | case 1: 640 | msg.msgStreamId = prevChunk.msgStreamId 641 | case 2, 3: 642 | msg.msgLength = prevChunk.msgLength 643 | msg.msgTypeId = prevChunk.msgTypeId 644 | msg.msgStreamId = prevChunk.msgStreamId 645 | } 646 | 647 | info[msg.csId] = chunkInfo{ 648 | timestampField: msg.timestampField, 649 | timestampDelta: msg.timestampDelta, 650 | timestampActual: msg.timestampActual, 651 | msgLength: msg.msgLength, 652 | msgTypeId: msg.msgTypeId, 653 | msgStreamId: msg.msgStreamId, 654 | } 655 | 656 | ts = msg.timestampActual 657 | 658 | msg.readingBody = true 659 | 660 | // rtmp payload 661 | for { 662 | if err = readChunkBody(rdr, &msg, csz); err != nil { 663 | return 664 | } 665 | 666 | if msg.msgLength <= msg.bodyBuff.Len() { 667 | break 668 | } 669 | 670 | //if err = decodeFmtCsId(rdr, &msg); err != nil { 671 | if err = decodeHeader(rdr, &msg); err != nil { 672 | return 673 | } 674 | 675 | // timestamp extended 676 | if (msg.timestampField == 0xffffff) { 677 | if err = decodeTimestampEX(rdr, &msg); err != nil { 678 | return 679 | } 680 | } 681 | } 682 | 683 | //fmt.Printf("debug rtmp decodeOne: %#v\n", msg) 684 | // read byte count 685 | rsz = msg.hdrLength + msg.msgLength 686 | 687 | msg_t = msg.msgTypeId 688 | switch msg.msgTypeId { 689 | case TID_AGGREGATE: 690 | if res, err = decodeAggregate(msg.bodyBuff); err != nil { 691 | return 692 | } 693 | 694 | case TID_AUDIO, TID_VIDEO: 695 | res = msg.bodyBuff 696 | 697 | case TID_WINDOW_ACK_SIZE: 698 | if res, err = decodeWindowAckSize(msg.bodyBuff); err != nil { 699 | return 700 | } 701 | case TID_SETPEERBANDWIDTH: 702 | if res, err = decodeSetPeerBandwidth(msg.bodyBuff); err != nil { 703 | return 704 | } 705 | case TID_AMF0COMMAND: 706 | if res, err = amf.DecodeAmf0(msg.bodyBuff.Bytes()); err != nil { 707 | return 708 | } 709 | case TID_AMF3COMMAND: 710 | if res, err = amf.DecodeAmf0(msg.bodyBuff.Bytes(), true); err != nil { 711 | return 712 | } 713 | case TID_AMF0DATA: 714 | if res, err = amf.DecodeAmf0(msg.bodyBuff.Bytes()); err != nil { 715 | return 716 | } 717 | case TID_SETCHUNKSIZE: 718 | if res, err = decodeSetChunkSize(msg.bodyBuff); err != nil { 719 | return 720 | } 721 | case TID_USERCONTROL: 722 | if res, err = decodeUserControl(msg.bodyBuff); err != nil { 723 | return 724 | } 725 | default: 726 | err = &DecodeError{ 727 | Fun: "decodeOne", 728 | Msg: fmt.Sprintf("msgTypeId: not implement: %v\n%#v", msg.msgTypeId, msg.bodyBuff.Bytes()), 729 | } 730 | return 731 | } 732 | 733 | return 734 | } -------------------------------------------------------------------------------- /src/rtmps/rtmp.go: -------------------------------------------------------------------------------- 1 | package rtmps 2 | 3 | import ( 4 | "net" 5 | "fmt" 6 | "bytes" 7 | "math/rand" 8 | "time" 9 | "io" 10 | "io/ioutil" 11 | "regexp" 12 | "../amf" 13 | "../flvs" 14 | "../objs" 15 | "../files" 16 | ) 17 | 18 | type DecodeError struct { 19 | Fun string 20 | Msg string 21 | } 22 | func (e *DecodeError) Error() string { 23 | return fmt.Sprintf("%s: %s", e.Fun, e.Msg) 24 | } 25 | 26 | type chunkInfo struct { 27 | timestampField int 28 | timestampDelta int 29 | timestampActual int 30 | msgLength int 31 | msgTypeId int 32 | msgStreamId int 33 | } 34 | 35 | type Rtmp struct { 36 | proto string // No reset 37 | address string // No reset 38 | app string // No reset 39 | tcUrl string // No reset 40 | swfUrl string // No reset 41 | pageUrl string // No reset 42 | connectOpt []interface{} 43 | 44 | conn *net.TCPConn // RESET_ON_CONNECT 45 | chunkSizeSend int // RESET_ON_CONNECT 46 | chunkSizeRecv int // RESET_ON_CONNECT 47 | transactionId int // RESET_ON_CONNECT 48 | windowSize int // RESET_ON_CONNECT 49 | chunkInfo map[int] chunkInfo // RESET_ON_CONNECT 50 | 51 | readCount int // RESET_ON_CONNECT 52 | totalReadBytes int // RESET_ON_CONNECT 53 | isRecorded bool 54 | 55 | timestamp int // NO_RESET 56 | duration int 57 | 58 | flvName string 59 | flv *flvs.Flv 60 | 61 | fixAggrTimestamp bool 62 | streamId int 63 | nextLogTs int 64 | 65 | VideoExists bool 66 | noSeek bool 67 | flush bool 68 | 69 | startTime int 70 | } 71 | 72 | func NewRtmp(tc, swf, page string, opt... interface{})(rtmp *Rtmp, err error) { 73 | re := regexp.MustCompile(`\A(\w+)://([^/\s]+)/(\S+)\z`) 74 | mstr := re.FindStringSubmatch(tc) 75 | if mstr == nil { 76 | err = fmt.Errorf("tcUrl incorrect: %v", tc) 77 | return 78 | } 79 | 80 | rtmp = &Rtmp{ 81 | proto: mstr[1], 82 | address: mstr[2], 83 | app: mstr[3], 84 | tcUrl: tc, 85 | swfUrl: swf, 86 | pageUrl: page, 87 | connectOpt: opt, 88 | } 89 | 90 | return 91 | } 92 | func (rtmp *Rtmp) Connect() (err error) { 93 | if rtmp.conn != nil { 94 | rtmp.conn.Close() 95 | rtmp.conn = nil 96 | time.Sleep(3) 97 | } 98 | 99 | rtmp.windowSize = 2500000 100 | rtmp.chunkInfo = make(map[int] chunkInfo) 101 | rtmp.chunkSizeSend = 128 102 | rtmp.chunkSizeRecv = 128 103 | rtmp.transactionId = 1 104 | 105 | rtmp.readCount = 0 106 | rtmp.totalReadBytes = 0 107 | 108 | err = rtmp.connect( 109 | rtmp.app, 110 | rtmp.tcUrl, 111 | rtmp.swfUrl, 112 | rtmp.pageUrl, 113 | rtmp.connectOpt..., 114 | ) 115 | return 116 | } 117 | func (rtmp *Rtmp) SetFlush(b bool) { 118 | rtmp.flush = b 119 | } 120 | func (rtmp *Rtmp) SetNoSeek(b bool) { 121 | rtmp.noSeek = b 122 | } 123 | func (rtmp *Rtmp) SetConnectOpt(opt... interface{}) { 124 | rtmp.connectOpt = opt 125 | } 126 | func (rtmp *Rtmp) connect(app, tc, swf, page string, opt... interface{}) (err error) { 127 | 128 | raddr, err := net.ResolveTCPAddr("tcp", rtmp.address) 129 | if err != nil { 130 | fmt.Printf("%v\n", err) 131 | return 132 | } 133 | 134 | switch rtmp.proto { 135 | case "rtmp": 136 | conn, e := net.DialTCP("tcp", nil, raddr) 137 | if e != nil { 138 | err = e 139 | return 140 | } 141 | rtmp.conn = conn 142 | 143 | default: 144 | err = fmt.Errorf("Unknown protocol: %v", rtmp.proto) 145 | return 146 | } 147 | 148 | err = handshake(rtmp.conn) 149 | if err != nil { 150 | rtmp.conn.Close() 151 | return 152 | } 153 | 154 | var data []interface{} 155 | data = append(data, map[string]interface{} { 156 | "app" : app, 157 | "flashVer" : "WIN 29,0,0,113", 158 | "swfUrl" : swf, 159 | "tcUrl" : tc, 160 | "fpad" : false, 161 | "capabilities" : 239, 162 | "audioCodecs" : 0xFFF, //3575, 163 | "videoCodecs" : 0xFF, //252, 164 | "videoFunction" : 1, 165 | "pageUrl" : page, 166 | "objectEncoding": 3, 167 | }) 168 | 169 | for _, o := range opt { 170 | data = append(data, o) 171 | } 172 | 173 | _, err = rtmp.Command("connect", data) 174 | 175 | return 176 | } 177 | 178 | const ( 179 | NORMAL = iota 180 | COMMAND 181 | PAUSE 182 | TEST 183 | ) 184 | func (rtmp *Rtmp) wait(findTrId int, pause bool, testTimeout int) (done, incomplete bool, trData interface{}, err error) { 185 | var mode int 186 | var endUnix int64 187 | var endTime time.Time 188 | if findTrId >= 0 { 189 | mode = COMMAND 190 | } else if pause { 191 | mode = PAUSE 192 | } else if testTimeout > 0 { 193 | mode = TEST 194 | endUnix = time.Now().Unix() + int64(testTimeout) 195 | endTime = time.Unix(endUnix, 0) 196 | } 197 | 198 | if mode != COMMAND { 199 | findTrId = -1 200 | } 201 | 202 | for { 203 | if mode == TEST { 204 | rtmp.conn.SetReadDeadline(endTime) 205 | } else { 206 | rtmp.conn.SetReadDeadline(time.Now().Add(300 * time.Second)) 207 | } 208 | __done, __incomplete, trFound, pause, __trData, e := rtmp.recvChunk(findTrId, pause) 209 | 210 | if e != nil { 211 | err = e 212 | return 213 | } 214 | if __done || __incomplete { 215 | done = __done 216 | incomplete = __incomplete 217 | return 218 | } 219 | 220 | switch mode { 221 | case COMMAND: 222 | if trFound { 223 | trData = __trData 224 | return 225 | } 226 | case PAUSE: 227 | if pause { 228 | return 229 | } 230 | case TEST: 231 | if time.Now().Unix() >= endUnix { 232 | return 233 | } 234 | } 235 | } 236 | } 237 | 238 | func (rtmp *Rtmp) WaitPause() (done, incomplete bool, err error) { 239 | done, incomplete, _, err = rtmp.wait(-1, true, -1) 240 | return 241 | } 242 | func (rtmp *Rtmp) WaitTest(testTimeout int) (done, incomplete bool, err error) { 243 | done, incomplete, _, err = rtmp.wait(-1, false, testTimeout) 244 | return 245 | } 246 | func (rtmp *Rtmp) Wait() (done, incomplete bool, err error) { 247 | done, incomplete, _, err = rtmp.wait(-1, false, -1) 248 | return 249 | } 250 | func (rtmp *Rtmp) waitCommand(findTrId int) (done, incomplete bool, trData interface{}, err error) { 251 | done, incomplete, trData, err = rtmp.wait(findTrId, false, -1) 252 | return 253 | } 254 | func (rtmp *Rtmp) SetFlvName(name string) { 255 | rtmp.flvName = name 256 | } 257 | func (rtmp *Rtmp) openFlv(incr bool) (err error) { 258 | if rtmp.flvName == "" { 259 | err = fmt.Errorf("FLV file name not set: call SetFlvName(string)") 260 | return 261 | } 262 | var fileName string 263 | if incr { 264 | if fileName, err = files.GetFileNameNext(rtmp.flvName); err != nil { 265 | return 266 | } 267 | } else { 268 | fileName = rtmp.flvName 269 | } 270 | flv, err := flvs.Open(fileName) 271 | if err != nil { 272 | return 273 | } 274 | rtmp.flv = flv 275 | return 276 | } 277 | func (rtmp *Rtmp) GetTimestamp() int { 278 | return rtmp.timestamp 279 | } 280 | func (rtmp *Rtmp) SetTimestamp(t int) { 281 | rtmp.timestamp = t 282 | } 283 | func (rtmp *Rtmp) writeMetaData(body map[string]interface{}, ts int) (err error) { 284 | 285 | if rtmp.flv == nil { 286 | if err = rtmp.openFlv(false); err != nil { 287 | return 288 | } 289 | } 290 | 291 | //buf := new(bytes.Buffer) 292 | data := []interface{}{} 293 | data = append(data, "onMetaData") 294 | data = append(data, body) 295 | 296 | dat, err := amf.EncodeAmf0(data, true) 297 | //fmt.Printf("writeMetaData %v %#v\n", ts, dat) 298 | rdr := bytes.NewBuffer(dat) 299 | err = rtmp.flv.WriteMetaData(rdr, ts) 300 | return 301 | } 302 | func (rtmp *Rtmp) writeAudio(rdr *bytes.Buffer, ts int) (err error) { 303 | if rtmp.flv == nil { 304 | if err = rtmp.openFlv(false); err != nil { 305 | return 306 | } 307 | } 308 | err = rtmp.flv.WriteAudio(rdr, ts) 309 | return 310 | } 311 | func (rtmp *Rtmp) writeVideo(rdr *bytes.Buffer, ts int) (err error) { 312 | if rtmp.flv == nil { 313 | if err = rtmp.openFlv(false); err != nil { 314 | return 315 | } 316 | } /*else if (!rtmp.flv.VideoExists() && rtmp.flv.AudioExists()) && ts > 1000 { 317 | if err = rtmp.openFlv(true); err != nil { 318 | return 319 | } 320 | }*/ 321 | err = rtmp.flv.WriteVideo(rdr, ts) 322 | return 323 | } 324 | func (rtmp *Rtmp) SetFixAggrTimestamp(sw bool) { 325 | rtmp.fixAggrTimestamp = sw 326 | } 327 | func (rtmp *Rtmp) CheckStatus(label string, ts int, data interface{}, waitPause bool) (done, incomplete, pauseFound bool, err error) { 328 | code, ok := objs.FindString(data, "code") 329 | if (! ok) { 330 | err = fmt.Errorf("%s: code Not found", label) 331 | return 332 | } 333 | 334 | switch code { 335 | case "NetStream.Pause.Notify": 336 | if waitPause { 337 | pauseFound = true 338 | } 339 | case "NetStream.Unpause.Notify": 340 | case "NetStream.Play.Stop": 341 | case "NetStream.Play.Complete": 342 | fmt.Printf("NetStream.Play.Complete: last timestamp: %d(flv)\n", rtmp.flv.GetLastTimestamp()) 343 | if (ts + 1000) > rtmp.duration { 344 | done = true 345 | } else { 346 | incomplete = true 347 | } 348 | case "NetStream.Play.Start": 349 | case "NetStream.Play.Reset": 350 | case "NetStream.Seek.Notify": 351 | case "NetStream.Play.Failed": 352 | done = true 353 | default: 354 | fmt.Printf("[FIXME] Unknown Code: %s\n", code) 355 | } 356 | return 357 | } 358 | // trId: transaction id to find 359 | func (rtmp *Rtmp) recvChunk(findTrId int, waitPause bool) (done, incomplete, trFound, pauseFound bool, trData interface{}, err error) { 360 | ts, msg_t, res, rdbytes, err := decodeOne(rtmp.conn, rtmp.chunkSizeRecv, rtmp.chunkInfo) 361 | if err != nil { 362 | switch err.(type) { 363 | case *net.OpError: 364 | return 365 | case *DecodeError: 366 | // データを受信したが、パースエラーとなった場合はやり直したい 367 | fmt.Printf("Please retry: RTMP: %v\n", err.Error()) 368 | incomplete = true 369 | err = nil 370 | return 371 | } 372 | 373 | return 374 | } 375 | ts = ts + rtmp.startTime 376 | 377 | // byte counter for acknowledgement 378 | rtmp.totalReadBytes += rdbytes 379 | rtmp.readCount += rdbytes 380 | if rtmp.readCount >= (rtmp.windowSize / 2) { 381 | rtmp.readCount = 0 382 | if err = rtmp.acknowledgement(); err != nil { 383 | return 384 | } 385 | } 386 | 387 | // print play timestamp 388 | if true { 389 | if rtmp.duration > 0 { 390 | switch msg_t { 391 | case TID_AUDIO, TID_VIDEO, TID_AGGREGATE: 392 | if ts >= rtmp.nextLogTs { 393 | fmt.Printf("#%8d/%d(%4.1f%%) : %s\n", ts, rtmp.duration, float64(ts)/float64(rtmp.duration)*100, rtmp.flvName) 394 | rtmp.nextLogTs = ts + 10000 395 | } 396 | } 397 | } else { 398 | switch msg_t { 399 | case TID_AUDIO, TID_VIDEO, TID_AGGREGATE: 400 | if ts >= rtmp.nextLogTs { 401 | fmt.Printf("#%8d : %s\n", ts, rtmp.flvName) 402 | rtmp.nextLogTs = ts + 10000 403 | } 404 | } 405 | } 406 | } 407 | 408 | switch msg_t { 409 | case TID_AUDIO: 410 | if ts > rtmp.timestamp { 411 | rtmp.timestamp = ts 412 | } 413 | if err = rtmp.writeAudio(res.(*bytes.Buffer), ts); err != nil { 414 | return 415 | } 416 | 417 | case TID_VIDEO: 418 | if ts > rtmp.timestamp { 419 | rtmp.timestamp = ts 420 | } 421 | if err = rtmp.writeVideo(res.(*bytes.Buffer), ts); err != nil { 422 | return 423 | } 424 | 425 | case TID_AGGREGATE: 426 | if ts > rtmp.timestamp { 427 | rtmp.timestamp = ts 428 | } 429 | var fstTs int 430 | for i, v := range res.([]message) { 431 | var tsAggr int 432 | if rtmp.fixAggrTimestamp { 433 | var delta int 434 | if i == 0 { 435 | fstTs = v.timestamp 436 | } 437 | delta = v.timestamp - fstTs 438 | tsAggr = ts + delta 439 | //fmt.Printf("FixAggrTs: fixed(%d), delta(%d), ts(%d), mts(%d)\n", tsAggr, delta, ts, v.timestamp) 440 | } else { 441 | if i == 0 { 442 | if ts != v.timestamp { 443 | err = fmt.Errorf("aggregate timestamp incorrect: ts:(%v) vs aggr[0].ts(%v)", ts, v.timestamp) 444 | return 445 | } 446 | } 447 | tsAggr = v.timestamp 448 | } 449 | 450 | if /*rtmp.isRecorded &&*/ rtmp.duration > 0 { 451 | switch v.msg_t { 452 | case TID_AUDIO, TID_VIDEO: 453 | // fmt.Printf(" %8d/%d(%4.1f%%) : %2d\n", tsAggr, rtmp.duration, float64(tsAggr)/float64(rtmp.duration)*100, v.msg_t) 454 | } 455 | } 456 | 457 | switch v.msg_t { 458 | case TID_AUDIO: 459 | // audio 460 | if err = rtmp.writeAudio(v.data, tsAggr); err != nil { 461 | return 462 | } 463 | 464 | case TID_VIDEO: 465 | // video 466 | if err = rtmp.writeVideo(v.data, tsAggr); err != nil { 467 | return 468 | } 469 | } 470 | } 471 | 472 | case TID_AMF0DATA, TID_AMF3DATA: 473 | objs.PrintAsJson(res) 474 | list, ok := res.([]interface{}) 475 | if (! ok) { 476 | err = fmt.Errorf("result AMF Data is not array") 477 | return 478 | } 479 | 480 | if len(list) >= 2 { 481 | name, ok := list[0].(string) 482 | if (! ok) { 483 | err = fmt.Errorf("result AMF Data[0] is not string") 484 | return 485 | } 486 | 487 | switch name { 488 | case "onPlayStatus": 489 | done, incomplete, pauseFound, err = rtmp.CheckStatus("onPlayStatus", ts, list[1], waitPause) 490 | 491 | case "onMetaData": 492 | dur, ok := objs.FindFloat64(list[1], "duration") 493 | if ok { 494 | rtmp.duration = int(dur * 1000) 495 | } else { 496 | if rtmp.isRecorded { 497 | fmt.Println("[WARN] onMetaData: duration not found") 498 | } 499 | } 500 | if meta, ok := list[1].(map[string]interface{}); ok { 501 | rtmp.writeMetaData(meta, ts) 502 | } 503 | 504 | _, ok = objs.Find(list[1], "videoframerate") 505 | if ok { 506 | rtmp.VideoExists = true 507 | } 508 | } 509 | } 510 | 511 | case TID_AMF0COMMAND, TID_AMF3COMMAND: 512 | objs.PrintAsJson(res) 513 | 514 | list, ok := res.([]interface{}) 515 | if (! ok) { 516 | err = fmt.Errorf("result AMF Command is not array") 517 | return 518 | } 519 | 520 | if len(list) >= 3 { 521 | name, ok := list[0].(string) 522 | if (! ok) { 523 | err = fmt.Errorf("result AMF Command name is not string") 524 | return 525 | } 526 | trIdFloat, ok := list[1].(float64) 527 | if (! ok) { 528 | err = fmt.Errorf("result AMF Command transaction id is not number") 529 | return 530 | } 531 | trId := int(trIdFloat) 532 | if (trId > 0) && (trId == findTrId) { 533 | trFound = true 534 | if len(list) >= 4 { 535 | trData = list[3] 536 | } 537 | } 538 | 539 | switch name { 540 | case "_error", "close": 541 | err = fmt.Errorf("AMF command not success: transaction id(%d) -> %s", trId, name) 542 | return 543 | case "onStatus": 544 | done, incomplete, pauseFound, err = rtmp.CheckStatus("onStatus", ts, list[3], waitPause) 545 | } 546 | } 547 | 548 | case TID_SETCHUNKSIZE: 549 | rtmp.chunkSizeRecv = res.(int) 550 | 551 | case TID_WINDOW_ACK_SIZE: 552 | rtmp.windowSize = res.(int) 553 | 554 | case TID_USERCONTROL: 555 | switch res.([]int)[0] { 556 | case UC_PINGREQUEST: 557 | //fmt.Printf("ping request %d\n", res.([]int)[1]) 558 | if err = rtmp.pingResponse(res.([]int)[1]); err != nil { 559 | return 560 | } 561 | 562 | case UC_STREAMBEGIN: 563 | rtmp.streamId = res.([]int)[1] 564 | 565 | case UC_STREAMISRECORDED: 566 | fmt.Printf("stream is recorded\n") 567 | rtmp.isRecorded = true 568 | 569 | case UC_BUFFEREMPTY: 570 | if rtmp.isRecorded { 571 | fmt.Printf("required Seek: %d\n", rtmp.timestamp) 572 | // <-- test 573 | rtmp.PauseRaw() 574 | incomplete = true 575 | return 576 | // test --> 577 | 578 | if rtmp.noSeek { 579 | incomplete = true 580 | return 581 | } 582 | ts := rtmp.timestamp - 10000 583 | if ts < 0 { 584 | ts = 0 585 | } 586 | done, incomplete, err = rtmp.PauseUnpause(ts) 587 | if done || incomplete || err != nil { 588 | return 589 | } 590 | //rtmp.Seek(ts) 591 | } 592 | } 593 | default: 594 | //fmt.Printf("got: %8d %d %#v\n", ts, msg_t, res) 595 | } 596 | return 597 | } 598 | 599 | func (rtmp *Rtmp) Close() (err error) { 600 | if rtmp.conn != nil { 601 | err = rtmp.conn.Close() 602 | } 603 | if rtmp.flv != nil { 604 | rtmp.flv.Close() 605 | } 606 | return 607 | } 608 | 609 | func (rtmp *Rtmp) SetPeerBandwidth(wsz, lim int) (err error) { 610 | buff, err := encodeSetPeerBandwidth(wsz, lim) 611 | if err != nil { 612 | return 613 | } 614 | if _, err = buff.WriteTo(rtmp.conn); err != nil { 615 | return 616 | } 617 | return 618 | } 619 | 620 | 621 | func (rtmp *Rtmp) pingResponse(timestamp int) (err error) { 622 | buff, err := encodePingResponse(timestamp) 623 | if _, err = buff.WriteTo(rtmp.conn); err != nil { 624 | return 625 | } 626 | return 627 | } 628 | func (rtmp *Rtmp) acknowledgement() (err error) { 629 | buff, err := encodeAcknowledgement(rtmp.totalReadBytes) 630 | if _, err = buff.WriteTo(rtmp.conn); err != nil { 631 | return 632 | } 633 | return 634 | } 635 | func (rtmp *Rtmp) WindowAckSize(asz int) (err error) { 636 | buff, err := encodeWindowAckSize(asz) 637 | if _, err = buff.WriteTo(rtmp.conn); err != nil { 638 | return 639 | } 640 | return 641 | } 642 | func (rtmp *Rtmp) SetBufferLength(streamId, len int) (err error) { 643 | buff, err := encodeSetBufferLength(streamId, len) 644 | if _, err = buff.WriteTo(rtmp.conn); err != nil { 645 | return 646 | } 647 | return 648 | } 649 | 650 | // command name, transaction ID, and command object 651 | func (rtmp *Rtmp) Command(name string, args []interface{}) (trData interface{}, err error) { 652 | var trId int 653 | var csId int 654 | var streamId int 655 | switch name { 656 | case "connect": 657 | rtmp.transactionId = 1 658 | trId = rtmp.transactionId 659 | csId = 3 660 | streamId = 0 661 | 662 | case "play", "seek", "pause", "pauseRaw": 663 | trId = 0 664 | csId = 8 665 | streamId = 1 666 | 667 | default: 668 | // createStream, call, close, ... 669 | rtmp.transactionId++ 670 | trId = rtmp.transactionId 671 | csId = 3 672 | streamId = 0 673 | } 674 | cmd := []interface{}{name, trId} 675 | cmd = append(cmd, args...) 676 | objs.PrintAsJson(cmd) 677 | body, err := amf.EncodeAmf0(cmd, false) 678 | wbuff, err := amf0Command(rtmp.chunkSizeSend, csId, streamId, body) 679 | 680 | if _, err = wbuff.WriteTo(rtmp.conn); err != nil { 681 | return 682 | } 683 | 684 | if trId > 0 { 685 | if _, _, trData, err = rtmp.waitCommand(trId); err != nil { 686 | return 687 | } 688 | } 689 | 690 | return 691 | } 692 | 693 | func (rtmp *Rtmp) Unpause(timestamp int) (err error) { 694 | var data []interface{} 695 | data = append(data, nil) 696 | data = append(data, false) 697 | data = append(data, timestamp) 698 | 699 | _, err = rtmp.Command("pause", data) 700 | 701 | return 702 | } 703 | func (rtmp *Rtmp) Pause(timestamp int) (err error) { 704 | var data []interface{} 705 | data = append(data, nil) 706 | data = append(data, true) 707 | data = append(data, timestamp) 708 | 709 | _, err = rtmp.Command("pause", data) 710 | 711 | return 712 | } 713 | func (rtmp *Rtmp) PauseRaw() (err error) { 714 | _, err = rtmp.Command("pauseRaw", []interface{}{ 715 | nil, 716 | true, 717 | 0, 718 | }) 719 | 720 | return 721 | } 722 | func (rtmp *Rtmp) PauseUnpause(timestamp int) (done, incomplete bool, err error) { 723 | if err = rtmp.Pause(timestamp); err != nil { 724 | return 725 | } 726 | fmt.Println("paused") 727 | done, incomplete, err = rtmp.WaitPause() 728 | if done || incomplete || err != nil { 729 | return 730 | } 731 | fmt.Println("wait pause") 732 | if err = rtmp.Unpause(timestamp); err != nil { 733 | return 734 | } 735 | fmt.Println("Unpaused") 736 | return 737 | } 738 | func (rtmp *Rtmp) PlayTime(stream string, timestamp int) (err error) { 739 | 740 | rtmp.startTime = timestamp 741 | if rtmp.startTime < 0 { 742 | rtmp.startTime = 0 743 | } 744 | //fmt.Printf("debug rtmp.startTime: %d\n", rtmp.startTime) 745 | 746 | var data []interface{} 747 | data = append(data, nil) 748 | data = append(data, stream) 749 | 750 | data = append(data, timestamp) // Start 751 | // NicoOfficialTs, Never append Duration and flush 752 | if rtmp.flush { 753 | data = append(data, -1) // Duration 754 | data = append(data, true) // flush 755 | } 756 | 757 | _, err = rtmp.Command("play", data) 758 | 759 | return 760 | } 761 | func (rtmp *Rtmp) Play(stream string) error { 762 | return rtmp.PlayTime(stream, -5000) 763 | } 764 | func (rtmp *Rtmp) Seek(timestamp int) (err error) { 765 | //fmt.Printf("debug Seek to %d\n", timestamp) 766 | var data []interface{} 767 | data = append(data, nil) 768 | data = append(data, timestamp) 769 | 770 | _, err = rtmp.Command("seek", data) 771 | 772 | //fmt.Printf("debug Seek done\n") 773 | return 774 | } 775 | func (rtmp *Rtmp) CreateStream() (err error) { 776 | var data []interface{} 777 | data = append(data, nil) 778 | 779 | _, err = rtmp.Command("createStream", data) 780 | 781 | return 782 | } 783 | 784 | func handshake(conn *net.TCPConn) (err error) { 785 | 786 | wbuff := bytes.NewBuffer(nil) 787 | 788 | // C0 789 | wbuff.WriteByte(3) 790 | // C1 791 | rnd := rand.New(rand.NewSource(time.Now().UnixNano())) 792 | io.CopyN(wbuff, rnd, 1536) 793 | 794 | // Send C0+C1 795 | if _, err = wbuff.WriteTo(conn); err != nil { 796 | return 797 | } 798 | 799 | // Recv S0 800 | if _, err = io.CopyN(ioutil.Discard, conn, 1); err != nil { 801 | return 802 | } 803 | 804 | // Recv S1 805 | if _, err = io.CopyN(wbuff, conn, 1536); err != nil { 806 | return 807 | } 808 | 809 | // Send C2(=S1) 810 | if _, err = wbuff.WriteTo(conn); err != nil { 811 | return 812 | } 813 | // Recv S2 814 | if _, err = io.CopyN(ioutil.Discard, conn, 1536); err != nil { 815 | return 816 | } 817 | return 818 | } 819 | 820 | 821 | -------------------------------------------------------------------------------- /src/twitcas/twicas.go: -------------------------------------------------------------------------------- 1 | package twitcas 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "time" 12 | "database/sql" 13 | _ "github.com/mattn/go-sqlite3" 14 | "github.com/gorilla/websocket" 15 | "../files" 16 | "../httpbase" 17 | "../procs/ffmpeg" 18 | "os/exec" 19 | "io" 20 | ) 21 | 22 | type Twitcas struct { 23 | Conn *websocket.Conn 24 | } 25 | 26 | func connectStream(proto, host, mode string, id uint64, proxy string) (conn *websocket.Conn, err error) { 27 | streamUrl := fmt.Sprintf( 28 | //case A.InnerFrame:return"i"; 29 | //case A.Pframe:return"p"; 30 | //case A.DisposableProfile:return"bd"; 31 | //case A.Bframe:return"b"; 32 | //case A.Any:return"any"; 33 | //case A.KeyFrame:default:return"k"} 34 | //"%s://%s/ws.app/stream/%d/fmp4/k/0/1?mode=%s", 35 | "%s://%s/ws.app/stream/%d/fmp4/bd/1/1500?mode=%s", 36 | proto, host, id, mode, 37 | ) 38 | // fmt.Println(streamUrl) 39 | 40 | var origin string 41 | if proto == "wss" { 42 | origin = fmt.Sprintf("https://%s", host) 43 | } else { 44 | origin = fmt.Sprintf("http://%s", host) 45 | } 46 | 47 | header := http.Header{} 48 | header.Set("Origin", origin) 49 | //header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36") 50 | header.Set("User-Agent", httpbase.GetUserAgent()) 51 | 52 | timeout, _ := time.ParseDuration("10s") 53 | dialer := websocket.Dialer{ 54 | HandshakeTimeout: timeout, 55 | } 56 | if proxy != "" { 57 | dialer.Proxy = func(req *http.Request) (u *url.URL, err error) { 58 | var proxyUrl string 59 | if proto == "wss" { 60 | proxyUrl = fmt.Sprintf("https://%s", proxy) 61 | } else { 62 | proxyUrl = fmt.Sprintf("http://%s", proxy) 63 | } 64 | return url.ParseRequestURI(proxyUrl) 65 | } 66 | } 67 | 68 | conn, _, err = dialer.Dial(streamUrl, header) 69 | 70 | return 71 | } 72 | 73 | func getStream(user, proxy string) (conn *websocket.Conn, movieId uint64, err error) { 74 | url := fmt.Sprintf( 75 | "https://twitcasting.tv/streamserver.php?target=%s&mode=client", 76 | user, 77 | ) 78 | 79 | type StreamServer struct { 80 | Movie struct { 81 | Id uint64 `json:"id"` 82 | Live bool `json:"live"` 83 | } `json:"movie"` 84 | Fmp4 struct { 85 | Host string `json:"host"` 86 | Proto string `json:"proto"` 87 | Source bool `json:"source"` 88 | MobileSource bool `json:"mobilesource"` 89 | } `json:"fmp4"` 90 | } 91 | 92 | req, err := http.NewRequest("GET", url, nil) 93 | if err != nil { 94 | return 95 | } 96 | 97 | client := new(http.Client) 98 | client.Timeout, _ = time.ParseDuration("10s") 99 | 100 | resp, err := client.Do(req) 101 | if err != nil { 102 | return 103 | } 104 | 105 | defer resp.Body.Close() 106 | respBytes, err := ioutil.ReadAll(resp.Body) 107 | if err != nil { 108 | return 109 | } 110 | //fmt.Printf("debug %s\n", string(respBytes)) 111 | 112 | data := new(StreamServer) 113 | 114 | err = json.Unmarshal(respBytes, data) 115 | if err != nil { 116 | return 117 | } 118 | 119 | if !data.Movie.Live { 120 | // movie not active 121 | err = errors.New(user + " --> " + "Offline or User Not Found") 122 | return 123 | } else { 124 | var mode string 125 | if data.Fmp4.Source { 126 | // StreamQuality.High 127 | mode = "main" 128 | } else if data.Fmp4.MobileSource { 129 | // StreamQuality.Middle 130 | mode = "mobilesource" 131 | } else { 132 | // StreamQuality.Low 133 | mode = "base" 134 | } 135 | 136 | if data.Fmp4.Proto != "" && data.Fmp4.Host != "" && data.Movie.Id != 0 { 137 | conn, err = connectStream(data.Fmp4.Proto, data.Fmp4.Host, mode, data.Movie.Id, proxy) 138 | if err != nil { 139 | return 140 | } 141 | movieId = data.Movie.Id 142 | } else { 143 | err = errors.New(user + " --> " + "No Stream Defined") 144 | return 145 | } 146 | } 147 | 148 | return 149 | } 150 | 151 | func createFileUser(user string, movieId uint64) (f *os.File, filename string, err error) { 152 | user = files.ReplaceForbidden(user) 153 | filename = fmt.Sprintf("%s_%d.mp4", user, movieId) 154 | for i := 2; i < 1000; i++ { 155 | _, err := os.Stat(filename) 156 | if err != nil { 157 | break 158 | } 159 | filename = fmt.Sprintf("%s_%d_%d.mp4", user, movieId, i) 160 | } 161 | f, err = os.Create(filename) 162 | return 163 | } 164 | 165 | // FIXME: return codeの整理 166 | func TwitcasRecord(user, proxy string) (done, dbLocked bool) { 167 | conn, movieId, err := getStream(user, proxy) 168 | if err != nil { 169 | fmt.Printf("@err getStream: %v\n", err) 170 | return 171 | } 172 | if conn == nil { 173 | fmt.Println("[FIXME] conn is nil") 174 | return 175 | } 176 | defer conn.Close() 177 | 178 | dbName := fmt.Sprintf("tmp/tcas-%v-lock.db", movieId) 179 | files.MkdirByFileName(dbName) 180 | db, err := sql.Open("sqlite3", dbName) 181 | if err != nil { 182 | fmt.Println(err) 183 | return 184 | } 185 | defer db.Close() 186 | 187 | _, err = db.Exec(`BEGIN EXCLUSIVE`) 188 | if err != nil { 189 | dbLocked = true 190 | return 191 | } 192 | defer os.Remove(dbName) 193 | 194 | //func Open(opt... string) (cmd *exec.Cmd, stdin io.WriteCloser, err error) { 195 | var cmd *exec.Cmd 196 | var stdin io.WriteCloser 197 | 198 | var fileOpened bool 199 | 200 | filenameBase := fmt.Sprintf("%s_%d.ts", user, movieId) 201 | filenameBase = files.ReplaceForbidden(filenameBase) // fixed #8 202 | 203 | closeFF := func() { 204 | if stdin != nil { 205 | stdin.Close() 206 | } 207 | if cmd != nil { 208 | cmd.Wait() 209 | } 210 | stdin = nil 211 | cmd = nil 212 | } 213 | 214 | openFF := func() (err error) { 215 | closeFF() 216 | 217 | filename, err := files.GetFileNameNext(filenameBase) 218 | if err != nil { 219 | fmt.Println(err) 220 | return 221 | } 222 | 223 | c, in, err := ffmpeg.Open("-i", "-", "-c", "copy", "-y", filename) 224 | if err != nil { 225 | return 226 | } 227 | cmd = c 228 | stdin = in 229 | 230 | fileOpened = true 231 | return 232 | } 233 | 234 | for { 235 | conn.SetReadDeadline(time.Now().Add(60 * time.Second)) 236 | messageType, data, err := conn.ReadMessage() 237 | if err != nil { 238 | fmt.Printf("@err ReadMessage: %v\n\n", err) 239 | return 240 | } 241 | 242 | if messageType == 2 { 243 | if cmd == nil || stdin == nil { 244 | if err = openFF(); err != nil { 245 | fmt.Println(err) 246 | return 247 | } 248 | defer closeFF() 249 | } 250 | 251 | if _, err := stdin.Write(data); err != nil { 252 | fmt.Println(err) 253 | return 254 | } 255 | 256 | } else if messageType == 1 { 257 | 258 | type TextMessage struct { 259 | Code int `json:"code"` 260 | } 261 | msg := new(TextMessage) 262 | err = json.Unmarshal(data, msg) 263 | if err != nil { 264 | // json decode error 265 | fmt.Printf("@err %v\n", err) 266 | return 267 | } 268 | if (msg.Code == 100) || (msg.Code == 101) || (msg.Code == 110) { 269 | // ignore 270 | } else if msg.Code == 400 { // invalid_parameter 271 | return 272 | } else if msg.Code == 401 { // passcode_required 273 | return 274 | } else if msg.Code == 403 { //access_forbidden 275 | return 276 | } else if msg.Code == 500 { // offline 277 | return 278 | } else if msg.Code == 503 { // server_error 279 | return 280 | } else if msg.Code == 504 { // live_ended 281 | break 282 | } else { 283 | fmt.Printf("@FIXME %v\n\n", string(data)) 284 | return 285 | } 286 | } 287 | } 288 | 289 | closeFF() 290 | 291 | done = fileOpened 292 | return 293 | } 294 | -------------------------------------------------------------------------------- /src/youtube/comment.go: -------------------------------------------------------------------------------- 1 | package youtube 2 | 3 | import ( 4 | "fmt" 5 | "context" 6 | "time" 7 | "sync" 8 | "database/sql" 9 | _ "github.com/mattn/go-sqlite3" 10 | "strconv" 11 | "encoding/json" 12 | "log" 13 | "path/filepath" 14 | "os" 15 | "html" 16 | "io/ioutil" 17 | "../gorman" 18 | "../files" 19 | "../httpbase" 20 | "../objs" 21 | ) 22 | 23 | type OBJ = map[string]interface{} 24 | 25 | func getComment(gm *gorman.GoroutineManager, ctx context.Context, sig <-chan struct{}, isReplay bool, continuation, name string) (done bool) { 26 | 27 | dbName := files.ChangeExtention(name, "yt.sqlite3") 28 | db, err := dbOpen(ctx, dbName) 29 | if err != nil { 30 | fmt.Println(err) 31 | return 32 | } 33 | defer db.Close() 34 | 35 | mtx := &sync.Mutex{} 36 | 37 | testContinuation, count, _ := dbGetContinuation(ctx, db, mtx) 38 | if testContinuation != "" { 39 | continuation = testContinuation 40 | } 41 | 42 | var printTime int64 43 | 44 | MAINLOOP: for { 45 | select { 46 | case <-ctx.Done(): break MAINLOOP 47 | case <-sig: break MAINLOOP 48 | default: 49 | } 50 | timeoutMs, _done, err, neterr := func() (timeoutMs int, _done bool, err, neterr error) { 51 | var uri string 52 | if isReplay { 53 | uri = fmt.Sprintf("https://www.youtube.com/youtubei/v1/live_chat/get_live_chat_replay?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8") 54 | } else { 55 | uri = fmt.Sprintf("https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8") 56 | } 57 | 58 | postData := OBJ{ 59 | "context": OBJ{ 60 | "client": OBJ{ 61 | "clientName": "WEB", 62 | "clientVersion": "2.20210128.02.00", 63 | }, 64 | }, 65 | "continuation": continuation, 66 | } 67 | resp, err, neterr := httpbase.PostJson(uri, map[string]string { 68 | "Cookie": Cookie, 69 | "User-Agent": UserAgent, 70 | }, postData) 71 | if err != nil { 72 | return 73 | } 74 | if neterr != nil { 75 | return 76 | } 77 | buff, neterr := ioutil.ReadAll(resp.Body) 78 | resp.Body.Close() 79 | if neterr != nil { 80 | return 81 | } 82 | code := resp.StatusCode 83 | if code != 200 { 84 | if code == 404 { 85 | fmt.Printf("Status code: %v (ignored)\n", code) 86 | time.Sleep(1000 * time.Millisecond) 87 | return 88 | } else { 89 | neterr = fmt.Errorf("Status code: %v\n", code) 90 | return 91 | } 92 | } 93 | 94 | var data interface{} 95 | err = json.Unmarshal(buff, &data) 96 | if err != nil { 97 | err = fmt.Errorf("json decode error") 98 | return 99 | } 100 | 101 | liveChatContinuation, ok := objs.Find(data, "continuationContents", "liveChatContinuation") 102 | if (! ok) { 103 | err = fmt.Errorf("(response liveChatContinuation) not found") 104 | return 105 | } 106 | 107 | if actions, ok := objs.FindArray(liveChatContinuation, "actions"); ok { 108 | var videoOffsetTimeMsec string 109 | 110 | for _, a := range actions { 111 | var item interface{} 112 | var ok bool 113 | item, ok = objs.Find(a, "addChatItemAction", "item") 114 | if (! ok) { 115 | item, ok = objs.Find(a, "addLiveChatTickerItemAction", "item") 116 | if (! ok) { 117 | item, ok = objs.Find(a, "replayChatItemAction", "actions", "addChatItemAction", "item") 118 | if ok { 119 | videoOffsetTimeMsec, _ = objs.FindString(a, "replayChatItemAction", "videoOffsetTimeMsec") 120 | } 121 | } 122 | } 123 | if (! ok) { 124 | //objs.PrintAsJson(a) 125 | //fmt.Println("(actions item) not found") 126 | continue 127 | } 128 | 129 | var liveChatMessageRenderer interface{} 130 | liveChatMessageRenderer, ok = objs.Find(item, "liveChatTextMessageRenderer") 131 | if (! ok) { 132 | liveChatMessageRenderer, ok = objs.Find(item, "liveChatPaidMessageRenderer") 133 | } 134 | if (! ok) { 135 | continue 136 | } 137 | 138 | authorExternalChannelId, _ := objs.FindString(liveChatMessageRenderer, "authorExternalChannelId") 139 | authorName, _ := objs.FindString(liveChatMessageRenderer, "authorName", "simpleText") 140 | id, ok := objs.FindString(liveChatMessageRenderer, "id") 141 | if (! ok) { 142 | continue 143 | } 144 | var message string 145 | if runs, ok := objs.FindArray(liveChatMessageRenderer, "message", "runs"); ok { 146 | for _, run := range runs { 147 | if text, ok := objs.FindString(run, "text"); ok { 148 | message += text 149 | } else if emojis, ok := objs.FindArray(run, "emoji", "shortcuts"); ok { 150 | if emoji, ok := emojis[0].(string); ok { 151 | message += emoji 152 | } 153 | } 154 | } 155 | } 156 | var others string 157 | var amount string 158 | amount, _ = objs.FindString(liveChatMessageRenderer, "purchaseAmountText", "simpleText") 159 | if amount != "" { 160 | others += ` amount="` + html.EscapeString(amount) + `"` 161 | } 162 | timestampUsec, ok := objs.FindString(liveChatMessageRenderer, "timestampUsec") 163 | if (! ok) { 164 | continue 165 | } 166 | 167 | 168 | if false { 169 | fmt.Printf("%v ", videoOffsetTimeMsec) 170 | fmt.Printf("%v %v %v %v %v %v [%v ]\n", timestampUsec, count, authorName, authorExternalChannelId, message, id, others) 171 | } 172 | 173 | dbInsert(ctx, gm, db, mtx, 174 | id, 175 | timestampUsec, 176 | videoOffsetTimeMsec, 177 | authorName, 178 | authorExternalChannelId, 179 | message, 180 | continuation, 181 | others, 182 | count, 183 | ) 184 | count++ 185 | } 186 | 187 | // アーカイブ時、20秒毎に進捗を表示 188 | if videoOffsetTimeMsec != "" { 189 | now := time.Now().Unix() 190 | if now - printTime > 20 { 191 | printTime = now 192 | if msec, e := strconv.ParseInt(videoOffsetTimeMsec, 10, 64); e == nil { 193 | total := msec / 1000 194 | hour := total / 3600 195 | min := (total % 3600) / 60 196 | sec := (total % 3600) % 60 197 | fmt.Printf("comment pos: %02d:%02d:%02d\n", hour, min, sec) 198 | } 199 | } 200 | } 201 | 202 | //fmt.Println("------------") 203 | } 204 | 205 | if continuations, ok := objs.Find(liveChatContinuation, "continuations"); ok { 206 | //objs.PrintAsJson(continuations) 207 | 208 | if c, ok := objs.FindString(continuations, "timedContinuationData", "continuation"); ok { 209 | continuation = c 210 | } else if c, ok := objs.FindString(continuations, "liveChatReplayContinuationData", "continuation"); ok { 211 | continuation = c 212 | } else if c, ok := objs.FindString(continuations, "invalidationContinuationData", "continuation"); ok { 213 | continuation = c 214 | } else if c, ok := objs.FindString(continuations, "playerSeekContinuationData", "continuation"); ok { 215 | if isReplay { 216 | _done = true 217 | return 218 | } 219 | continuation = c 220 | } else { 221 | objs.PrintAsJson(continuations) 222 | err = fmt.Errorf("(liveChatContinuation continuation) not found") 223 | return 224 | } 225 | 226 | if t, ok := objs.FindString(continuations, "timedContinuationData", "timeoutMs"); ok { 227 | timeout, err := strconv.Atoi(t) 228 | if err != nil { 229 | timeoutMs = timeout 230 | } 231 | } else if t, ok := objs.FindString(continuations, "invalidationContinuationData", "continuation"); ok { 232 | timeout, err := strconv.Atoi(t) 233 | if err != nil { 234 | timeoutMs = timeout 235 | } 236 | } 237 | 238 | } else { 239 | objs.PrintAsJson(liveChatContinuation) 240 | err = fmt.Errorf("(liveChatContinuation>continuations) not found") 241 | return 242 | } 243 | 244 | return 245 | }() 246 | if err != nil { 247 | fmt.Println(err) 248 | break 249 | } 250 | if neterr != nil { 251 | fmt.Println(neterr) 252 | break 253 | } 254 | if _done { 255 | done = true 256 | break MAINLOOP 257 | } 258 | 259 | if timeoutMs < 1000 { 260 | if isReplay { 261 | timeoutMs = 1000 262 | } else { 263 | timeoutMs = 6000 264 | } 265 | } 266 | time.Sleep(time.Duration(timeoutMs) * time.Millisecond) 267 | } 268 | return 269 | } 270 | 271 | func dbOpen(ctx context.Context, name string) (db *sql.DB, err error) { 272 | db, err = sql.Open("sqlite3", name) 273 | if err != nil { 274 | return 275 | } 276 | 277 | _, err = db.ExecContext(ctx, ` 278 | PRAGMA synchronous = OFF; 279 | PRAGMA journal_mode = WAL; 280 | `) 281 | if err != nil { 282 | db.Close() 283 | return 284 | } 285 | 286 | err = dbCreate(ctx, db) 287 | if err != nil { 288 | db.Close() 289 | } 290 | return 291 | } 292 | 293 | func dbCreate(ctx context.Context, db *sql.DB) (err error) { 294 | // table media 295 | 296 | _, err = db.ExecContext(ctx, ` 297 | CREATE TABLE IF NOT EXISTS comment ( 298 | id TEXT PRIMARY KEY NOT NULL UNIQUE, 299 | timestampUsec INTEGER NOT NULL, 300 | videoOffsetTimeMsec INTEGER, 301 | authorName TEXT, 302 | channelId TEXT, 303 | message TEXT, 304 | continuation TEXT, 305 | others TEXT, 306 | count INTEGER NOT NULL 307 | ) 308 | `) 309 | if err != nil { 310 | return 311 | } 312 | 313 | _, err = db.ExecContext(ctx, ` 314 | CREATE UNIQUE INDEX IF NOT EXISTS comment0 ON comment(id); 315 | CREATE INDEX IF NOT EXISTS comment1 ON comment(timestampUsec); 316 | CREATE INDEX IF NOT EXISTS comment2 ON comment(videoOffsetTimeMsec); 317 | CREATE INDEX IF NOT EXISTS comment3 ON comment(count); 318 | `) 319 | if err != nil { 320 | return 321 | } 322 | 323 | return 324 | } 325 | 326 | func dbInsert(ctx context.Context, gm *gorman.GoroutineManager, db *sql.DB, mtx *sync.Mutex, 327 | id, timestampUsec, videoOffsetTimeMsec, authorName, authorExternalChannelId, message, continuation, others string, count int) { 328 | 329 | usec, err := strconv.ParseInt(timestampUsec, 10, 64) 330 | if err != nil { 331 | fmt.Printf("ParseInt error: %s\n", timestampUsec) 332 | return 333 | } 334 | var offset interface{} 335 | if videoOffsetTimeMsec == "" { 336 | offset = nil 337 | } else { 338 | n, err := strconv.ParseInt(videoOffsetTimeMsec, 10, 64) 339 | if err != nil { 340 | offset = nil 341 | } else { 342 | offset = n 343 | } 344 | } 345 | 346 | query := `INSERT OR IGNORE INTO comment 347 | (id, timestampUsec, videoOffsetTimeMsec, authorName, channelId, message, continuation, others, count) VALUES (?,?,?,?,?,?,?,?,?)` 348 | 349 | gm.Go(func(<-chan struct{}) int { 350 | mtx.Lock() 351 | defer mtx.Unlock() 352 | 353 | if _, err := db.ExecContext(ctx, query, 354 | id, usec, offset, authorName, authorExternalChannelId, message, continuation, others, count, 355 | ); err != nil { 356 | if err.Error() != "context canceled" { 357 | fmt.Println(err) 358 | } 359 | return 1 360 | } 361 | return 0 362 | }) 363 | 364 | return 365 | } 366 | 367 | func dbGetContinuation(ctx context.Context, db *sql.DB, mtx *sync.Mutex) (res string, cnt int, err error) { 368 | mtx.Lock() 369 | defer mtx.Unlock() 370 | 371 | err = db.QueryRowContext(ctx, "SELECT continuation, count FROM comment ORDER BY count DESC LIMIT 1").Scan(&res, &cnt) 372 | return 373 | } 374 | 375 | var SelComment = `SELECT 376 | timestampUsec, 377 | IFNULL(videoOffsetTimeMsec, -1), 378 | authorName, 379 | channelId, 380 | message, 381 | others, 382 | count 383 | FROM comment 384 | ORDER BY timestampUsec 385 | ` 386 | 387 | func WriteComment(db *sql.DB, fileName string) { 388 | 389 | rows, err := db.Query(SelComment) 390 | if err != nil { 391 | log.Println(err) 392 | return 393 | } 394 | defer rows.Close() 395 | 396 | fileName = files.ChangeExtention(fileName, "xml") 397 | 398 | dir := filepath.Dir(fileName) 399 | base := filepath.Base(fileName) 400 | base, err = files.GetFileNameNext(base) 401 | if err != nil { 402 | fmt.Println(err) 403 | os.Exit(1) 404 | } 405 | fileName = filepath.Join(dir, base) 406 | f, err := os.Create(fileName) 407 | if err != nil { 408 | log.Fatalln(err) 409 | } 410 | defer f.Close() 411 | fmt.Fprintf(f, "%s\r\n", ``) 412 | fmt.Fprintf(f, "%s\r\n", ``) 413 | 414 | firstOffsetUsec := int64(-1) 415 | 416 | for rows.Next() { 417 | var timestampUsec int64 418 | var videoOffsetTimeMsec int64 419 | var authorName string 420 | var channelId string 421 | var message string 422 | var others string 423 | var count int64 424 | 425 | err = rows.Scan( 426 | ×tampUsec, 427 | &videoOffsetTimeMsec, 428 | &authorName, 429 | &channelId, 430 | &message, 431 | &others, 432 | &count, 433 | ) 434 | if err != nil { 435 | log.Println(err) 436 | return 437 | } 438 | 439 | var vpos int64 440 | if videoOffsetTimeMsec >= 0 { 441 | vpos = videoOffsetTimeMsec / 10 442 | } else { 443 | if firstOffsetUsec < 0 { 444 | firstOffsetUsec = timestampUsec 445 | } 446 | diff := timestampUsec - firstOffsetUsec 447 | vpos = diff / (10 * 1000) 448 | } 449 | 450 | line := fmt.Sprintf( 451 | ``) 468 | } 469 | -------------------------------------------------------------------------------- /src/youtube/youtube.go: -------------------------------------------------------------------------------- 1 | package youtube 2 | 3 | import ( 4 | "fmt" 5 | // "net/http" 6 | // "io/ioutil" 7 | "bufio" 8 | "regexp" 9 | "encoding/json" 10 | "html" 11 | "context" 12 | "os" 13 | "os/signal" 14 | "syscall" 15 | "../objs" 16 | "../files" 17 | "../procs/streamlink" 18 | "../procs/youtube_dl" 19 | "../gorman" 20 | "../httpbase" 21 | 22 | "strings" 23 | "time" 24 | "sync" 25 | "../procs" 26 | ) 27 | 28 | var Cookie = "PREF=f1=50000000&f4=4000000&hl=en" 29 | var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36" 30 | 31 | var split = func(data []byte, atEOF bool) (advance int, token []byte, err error) { 32 | for i := 0; i < len(data) ; i++ { 33 | if data[i] == '\n' { 34 | return i + 1, data[:i + 1], nil 35 | } 36 | if data[i] == '\r' { 37 | if (i + 1) == len(data) { 38 | return 0, nil, nil 39 | } 40 | if data[i + 1] == '\n' { 41 | return i + 2, data[:i + 2], nil 42 | } 43 | return i + 1, data[:i + 1], nil 44 | } 45 | } 46 | 47 | if atEOF && len(data) > 0 { 48 | return len(data), data, nil 49 | } 50 | 51 | return 0, nil, nil 52 | } 53 | 54 | func getChatContinuation(buff []byte) (isReplay bool, continuation string, err error) { 55 | 56 | if ma := regexp.MustCompile(`(?s)\WytInitialData\p{Zs}*=\p{Zs}*({.*?})\p{Zs}*;`).FindSubmatch(buff); len(ma) > 1 { 57 | var data interface{} 58 | err = json.Unmarshal(ma[1], &data) 59 | if err != nil { 60 | err = fmt.Errorf("ytInitialData parse error") 61 | return 62 | } 63 | 64 | //objs.PrintAsJson(data); 65 | 66 | liveChatRenderer, ok := objs.Find(data, 67 | "contents", 68 | "twoColumnWatchNextResults", 69 | "conversationBar", 70 | "liveChatRenderer", 71 | ) 72 | if (! ok) { 73 | err = fmt.Errorf("liveChatRenderer not found") 74 | return 75 | } 76 | isReplay, _ = objs.FindBool(liveChatRenderer, "isReplay") 77 | 78 | subMenuItems, ok := objs.FindArray(liveChatRenderer, 79 | "header", 80 | "liveChatHeaderRenderer", 81 | "viewSelector", 82 | "sortFilterSubMenuRenderer", 83 | "subMenuItems", 84 | ) 85 | if (! ok) { 86 | err = fmt.Errorf("subMenuItems not found") 87 | return 88 | } 89 | 90 | for _, item := range subMenuItems { 91 | title, _ := objs.FindString(item, "title") 92 | //selected, _ := objs.FindBool(item, "selected") 93 | c, _ := objs.FindString(item, "continuation", "reloadContinuationData", "continuation") 94 | 95 | if (title != "") && (! strings.Contains(title, "Top")) { 96 | continuation = c 97 | return 98 | } 99 | continuation = c 100 | } 101 | 102 | } else { 103 | err = fmt.Errorf("ytInitialData not found") 104 | return 105 | } 106 | 107 | if continuation == "" { 108 | err = fmt.Errorf("continuation not found") 109 | } 110 | return 111 | } 112 | 113 | func getInfo(buff []byte) (title, ucid, author string, err error) { 114 | var data interface{} 115 | re := regexp.MustCompile(`(?s)\WytInitialPlayerResponse\p{Zs}*=\p{Zs}*({.*?})\p{Zs}*;`) 116 | if ma := re.FindSubmatch(buff); len(ma) > 1 { 117 | str := html.UnescapeString(string(ma[1])) 118 | if err = json.Unmarshal([]byte(str), &data); err != nil { 119 | err = fmt.Errorf("ytInitialPlayerResponse parse error") 120 | return 121 | } 122 | } else { 123 | err = fmt.Errorf("ytInitialPlayerResponse not found") 124 | return 125 | } 126 | 127 | //objs.PrintAsJson(data); return 128 | 129 | title, ok := objs.FindString(data, "videoDetails", "title") 130 | if (! ok) { 131 | err = fmt.Errorf("title not found") 132 | return 133 | } 134 | ucid, _ = objs.FindString(data, "videoDetails", "channelId") 135 | author, _ = objs.FindString(data, "videoDetails", "author") 136 | return 137 | } 138 | 139 | func execStreamlink(gm *gorman.GoroutineManager, uri, name string) (notSupport bool, err error) { 140 | cmd, stdout, stderr, err := streamlink.Open(uri, "best", "--retry-max", "10", "-o", name) 141 | if err != nil { 142 | return 143 | } 144 | defer stdout.Close() 145 | defer stderr.Close() 146 | 147 | chStdout := make(chan string, 10) 148 | chStderr := make(chan string, 10) 149 | chEof := make(chan struct{}, 2) 150 | 151 | // stdout 152 | gm.Go(func(c <-chan struct{}) int { 153 | defer func(){ 154 | chEof <- struct{}{} 155 | }() 156 | scanner := bufio.NewScanner(stdout) 157 | scanner.Split(split) 158 | 159 | for scanner.Scan() { 160 | chStdout <- scanner.Text() 161 | } 162 | 163 | return 0 164 | }) 165 | 166 | // stderr 167 | gm.Go(func(c <-chan struct{}) int { 168 | defer func(){ 169 | chEof <- struct{}{} 170 | }() 171 | scanner := bufio.NewScanner(stderr) 172 | scanner.Split(split) 173 | 174 | for scanner.Scan() { 175 | chStderr <- scanner.Text() 176 | } 177 | 178 | return 0 179 | }) 180 | 181 | 182 | // outputs 183 | gm.Go(func(c <-chan struct{}) int { 184 | for { 185 | var s string 186 | select { 187 | case s = <-chStdout: 188 | case s = <-chStderr: 189 | case <-chEof: 190 | return 0 191 | } 192 | 193 | if strings.HasPrefix(s, "[cli][error]") { 194 | fmt.Print(s) 195 | 196 | notSupport = true 197 | procs.Kill(cmd.Process.Pid) 198 | break 199 | } else if strings.HasPrefix(s, "Traceback (most recent call last):") { 200 | fmt.Print(s) 201 | 202 | notSupport = true 203 | //procs.Kill(cmd.Process.Pid) 204 | //break 205 | } else { 206 | fmt.Print(s) 207 | } 208 | } 209 | return 0 210 | }) 211 | 212 | cmd.Wait() 213 | 214 | return 215 | } 216 | 217 | func execYoutube_dl(gm *gorman.GoroutineManager, uri, name string) (err error) { 218 | defer func() { 219 | part := name + ".part" 220 | if _, test := os.Stat(part); test == nil { 221 | if _, test := os.Stat(name); test != nil { 222 | os.Rename(part, name) 223 | } 224 | } 225 | }() 226 | 227 | cmd, stdout, stderr, err := youtube_dl.Open("--no-mtime", "--no-color", "-o", name, uri) 228 | if err != nil { 229 | return 230 | } 231 | defer stdout.Close() 232 | defer stderr.Close() 233 | 234 | chStdout := make(chan string, 10) 235 | chStderr := make(chan string, 10) 236 | chEof := make(chan struct{}, 2) 237 | 238 | // stdout 239 | gm.Go(func(c <-chan struct{}) int { 240 | defer func(){ 241 | chEof <- struct{}{} 242 | }() 243 | scanner := bufio.NewScanner(stdout) 244 | scanner.Split(split) 245 | 246 | for scanner.Scan() { 247 | chStdout <- scanner.Text() 248 | } 249 | 250 | return 0 251 | }) 252 | 253 | // stderr 254 | gm.Go(func(c <-chan struct{}) int { 255 | defer func(){ 256 | chEof <- struct{}{} 257 | }() 258 | scanner := bufio.NewScanner(stderr) 259 | scanner.Split(split) 260 | 261 | for scanner.Scan() { 262 | chStderr <- scanner.Text() 263 | } 264 | 265 | return 0 266 | }) 267 | 268 | // outputs 269 | gm.Go(func(c <-chan struct{}) int { 270 | var old int64 271 | for { 272 | var s string 273 | select { 274 | case s = <-chStdout: 275 | case s = <-chStderr: 276 | case <-chEof: 277 | return 0 278 | } 279 | 280 | if strings.HasPrefix(s, "[https @ ") { 281 | // ffmpeg unwanted logs 282 | } else { 283 | if strings.HasPrefix(s, "[download]") { 284 | var now = time.Now().UnixNano() 285 | if now - old > 2 * 1000 * 1000 * 1000 { 286 | old = now 287 | } else { 288 | continue 289 | } 290 | } 291 | fmt.Print(s) 292 | } 293 | } 294 | return 0 295 | }) 296 | 297 | cmd.Wait() 298 | return 299 | } 300 | 301 | var COMMENT_DONE = 1000 302 | 303 | func Record(id string, ytNoStreamlink, ytNoYoutube_dl bool) (err error) { 304 | 305 | uri := fmt.Sprintf("https://www.youtube.com/watch?v=%s", id) 306 | code, buff, err, neterr := httpbase.GetBytes(uri, map[string]string { 307 | "Cookie": Cookie, 308 | "User-Agent": UserAgent, 309 | }) 310 | if err != nil { 311 | return 312 | } 313 | if neterr != nil { 314 | return 315 | } 316 | if code != 200 { 317 | neterr = fmt.Errorf("Status code: %v\n", code) 318 | return 319 | } 320 | 321 | title, ucid, author, err := getInfo(buff) 322 | if err != nil { 323 | return 324 | } 325 | 326 | if false { 327 | fmt.Println(ucid) 328 | } 329 | 330 | isReplay, continuation, err := getChatContinuation(buff) 331 | 332 | 333 | origName := fmt.Sprintf("%s-%s_%s.mp4", author, title, id) 334 | origName = files.ReplaceForbidden(origName) 335 | name, err := files.GetFileNameNext(origName) 336 | if err != nil { 337 | fmt.Println(err) 338 | return 339 | } 340 | 341 | fmt.Println(name) 342 | 343 | mtxComDone := &sync.Mutex{} 344 | var commentDone bool 345 | 346 | var gm *gorman.GoroutineManager 347 | var gmCom *gorman.GoroutineManager 348 | 349 | gm = gorman.WithChecker(func(c int) { 350 | switch c { 351 | case 0: 352 | default: 353 | gm.Cancel() 354 | if gmCom != nil { 355 | gmCom.Cancel(); 356 | } 357 | } 358 | }) 359 | 360 | gmCom = gorman.WithChecker(func(c int) { 361 | switch c { 362 | case 0: 363 | case COMMENT_DONE: 364 | func() { 365 | mtxComDone.Lock() 366 | defer mtxComDone.Unlock() 367 | commentDone = true; 368 | }() 369 | default: 370 | gmCom.Cancel() 371 | } 372 | }) 373 | 374 | chInterrupt := make(chan os.Signal, 10) 375 | signal.Notify(chInterrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 376 | defer signal.Stop(chInterrupt) 377 | 378 | ctx, cancel := context.WithCancel(context.Background()) 379 | 380 | var interrupt bool 381 | gm.Go(func(c <-chan struct{}) int { 382 | select { 383 | case <-chInterrupt: 384 | interrupt = true 385 | case <-c: 386 | } 387 | 388 | cancel() 389 | gm.Cancel() 390 | return 1 391 | }) 392 | 393 | if continuation != "" { 394 | gmCom.Go(func(c <-chan struct{}) int { 395 | getComment(gmCom, ctx, c, isReplay, continuation, origName) 396 | fmt.Printf("\ncomment done\n") 397 | return COMMENT_DONE 398 | }) 399 | } 400 | 401 | gm.Go(func(c <-chan struct{}) int { 402 | select { 403 | case <-c: cancel() 404 | } 405 | return 0 406 | }) 407 | 408 | var retry bool 409 | if (! ytNoStreamlink) { 410 | retry, err = execStreamlink(gm, uri, name) 411 | } 412 | if !interrupt { 413 | if err != nil || retry || (ytNoStreamlink && (! ytNoYoutube_dl)) { 414 | execYoutube_dl(gm, uri, name) 415 | } 416 | } 417 | 418 | if continuation != "" { 419 | if isReplay { 420 | if !commentDone { 421 | fmt.Printf("\nwaiting comment\n") 422 | gmCom.Wait() 423 | } else { 424 | gmCom.Wait() 425 | } 426 | 427 | } else { 428 | if !ytNoStreamlink || !ytNoYoutube_dl { 429 | gmCom.Cancel() 430 | } 431 | gmCom.Wait() 432 | } 433 | } 434 | 435 | gm.Cancel() 436 | gm.Wait() 437 | 438 | return 439 | } 440 | -------------------------------------------------------------------------------- /src/youtube/youtube.gox: -------------------------------------------------------------------------------- 1 | package youtube 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "io/ioutil" 8 | "regexp" 9 | "encoding/json" 10 | "html" 11 | "strings" 12 | "net/url" 13 | "os" 14 | "strconv" 15 | "bytes" 16 | "os/exec" 17 | "os/signal" 18 | "archive/zip" 19 | "sync" 20 | "../obj" 21 | "io" 22 | "../files" 23 | 24 | "../httpsub" 25 | "../zip2mp4" 26 | "log" 27 | ) 28 | 29 | type YtDash struct { 30 | SeqNo int 31 | SeqNoFound bool 32 | SeqNoBack int 33 | VAddr string 34 | VQuery url.Values 35 | AAddr string 36 | AQuery url.Values 37 | TsFile *os.File 38 | FFCmd *exec.Cmd 39 | FFBuffer *bytes.Buffer 40 | TryBack bool 41 | StartBack bool 42 | ChEnd chan bool 43 | ChEndBack chan bool 44 | zipFile *os.File 45 | zipWriter *zip.Writer 46 | mZip sync.Mutex 47 | fileName string 48 | Title string 49 | Id string 50 | } 51 | func (yt *YtDash) SetFileName(fileName string) { 52 | yt.fileName = files.ReplaceForbidden(fileName) 53 | } 54 | func (yt *YtDash) fetch(isVideo, isBack bool) (fileName string, err error) { 55 | 56 | var addr string 57 | var query url.Values 58 | var sn int 59 | 60 | if isVideo { 61 | addr = yt.VAddr 62 | query = yt.VQuery 63 | } else { 64 | addr = yt.AAddr 65 | query = yt.AQuery 66 | } 67 | 68 | if isBack && (! yt.SeqNoFound) { 69 | err = fmt.Errorf("isBack && (! SeqNoFound)") 70 | return 71 | } 72 | 73 | if yt.SeqNoFound { 74 | //fmt.Printf("SQ set to %d\n", yt.SeqNo) 75 | if isBack { 76 | sn = yt.SeqNoBack 77 | } else { 78 | sn = yt.SeqNo 79 | } 80 | query.Set("sq", fmt.Sprintf("%d", sn)) 81 | 82 | //fmt.Printf("%v\n", query) 83 | } 84 | 85 | uri := fmt.Sprintf("%s?%s", addr, query.Encode()) 86 | req, _ := http.NewRequest("GET", uri, nil) 87 | 88 | client := new(http.Client) 89 | resp, err := client.Do(req) 90 | if err != nil { 91 | return 92 | } 93 | defer resp.Body.Close() 94 | 95 | switch resp.StatusCode { 96 | case 200: 97 | default: 98 | err = fmt.Errorf("StatusCode is %v\n%v\n%v", resp.StatusCode, uri, query) 99 | return 100 | } 101 | 102 | switch query.Get("source") { 103 | case "yt_live_broadcast": 104 | 105 | bs, e := ioutil.ReadAll(resp.Body) 106 | if e != nil { 107 | err = e 108 | return 109 | } 110 | 111 | if (! yt.SeqNoFound) && (! isBack) { 112 | 113 | if ma := regexp.MustCompile(`Sequence-Number\s*:\s*(\d+)`).FindSubmatch(bs); len(ma) > 0 { 114 | sn, err = strconv.Atoi(string(ma[1])) 115 | if err != nil { 116 | err = fmt.Errorf("Sequence-Number Not a Number: %v", ma) 117 | return 118 | } 119 | yt.SeqNo = sn 120 | yt.SeqNoBack = sn - 1 121 | yt.SeqNoFound = true 122 | fmt.Printf("start SeqNo: %d\n", sn) 123 | 124 | } else { 125 | err = fmt.Errorf("Sequence-Number Not found") 126 | return 127 | } 128 | 129 | yt.RecordBack() 130 | } 131 | 132 | if isVideo { 133 | fileName = fmt.Sprintf("video-%d.mp4", sn) 134 | } else { 135 | fileName = fmt.Sprintf("audio-%d.mp4", sn) 136 | } 137 | 138 | buff := bytes.NewBuffer(bs) 139 | if err = yt.WriteZip(fileName, buff); err != nil { 140 | return 141 | } 142 | } 143 | 144 | return 145 | } 146 | func (yt *YtDash) fetchVideo() (string, error) { 147 | return yt.fetch(true, false) 148 | } 149 | func (yt *YtDash) fetchAudio() (string, error) { 150 | return yt.fetch(false, false) 151 | } 152 | func (yt *YtDash) IncrSeqNo() { 153 | yt.SeqNo++ 154 | } 155 | func (yt *YtDash) fetchVideoBack() (string, error) { 156 | return yt.fetch(true, true) 157 | } 158 | func (yt *YtDash) fetchAudioBack() (string, error) { 159 | return yt.fetch(false, true) 160 | } 161 | func (yt *YtDash) DecrSeqNoBack() { 162 | yt.SeqNoBack-- 163 | } 164 | func (yt *YtDash) RecordYoutube() { 165 | var vname string 166 | var aname string 167 | func() { 168 | uri := fmt.Sprintf("%s?%s", yt.VAddr, yt.VQuery.Encode()) 169 | vname = fmt.Sprintf("%s(%s)-v.mp4", yt.Title, yt.Id) 170 | fmt.Println(uri) 171 | sub := httpsub.Get(uri, vname) 172 | sub.Concurrent(4) 173 | sub.Wait() 174 | }() 175 | func() { 176 | uri := fmt.Sprintf("%s?%s", yt.AAddr, yt.AQuery.Encode()) 177 | aname = fmt.Sprintf("%s(%s)-a.mp4", yt.Title, yt.Id) 178 | sub := httpsub.Get(uri, aname) 179 | sub.Concurrent(4) 180 | sub.Wait() 181 | }() 182 | if zip2mp4.FFmpegExists() { 183 | exts := []string{"mp4", "mkv"} 184 | for _, ext := range exts { 185 | oname := fmt.Sprintf("%s(%s).%s", yt.Title, yt.Id, ext) 186 | if zip2mp4.MergeVA(vname, aname, oname) { 187 | os.Remove(vname) 188 | os.Remove(aname) 189 | return 190 | } else { 191 | os.Remove(oname) 192 | } 193 | } 194 | } 195 | // ffmpeg Not exists OR merge NG 196 | fv, e := os.Open(vname) 197 | if e != nil { 198 | log.Fatalln(e) 199 | } 200 | yt.WriteZip("video.mp4", fv) 201 | fv.Close() 202 | 203 | fa, e := os.Open(aname) 204 | if e != nil { 205 | log.Fatalln(e) 206 | } 207 | yt.WriteZip("audio.mp4", fa) 208 | fa.Close() 209 | 210 | os.Remove(vname) 211 | os.Remove(aname) 212 | 213 | } 214 | func (yt *YtDash) Wait() { 215 | 216 | yt.ChEnd = make(chan bool) 217 | yt.ChEndBack = make(chan bool) 218 | 219 | switch yt.VQuery.Get("source") { 220 | case "youtube": 221 | yt.RecordYoutube() 222 | 223 | case "yt_live_broadcast": 224 | yt.RecordForward() 225 | <-yt.ChEnd 226 | if yt.StartBack { 227 | <-yt.ChEndBack 228 | } 229 | } 230 | } 231 | func (yt *YtDash) Close() { 232 | if yt.zipWriter != nil { 233 | yt.zipWriter.Close() 234 | } 235 | if yt.zipFile != nil { 236 | yt.zipFile.Close() 237 | } 238 | } 239 | func (yt *YtDash) OpenFile() (err error) { 240 | 241 | fileName, err := files.GetFileNameNext(yt.fileName) 242 | if err != nil { 243 | return 244 | } 245 | 246 | file, err := os.Create(fileName) 247 | if err != nil { 248 | log.Fatalln(err) 249 | } 250 | yt.zipFile = file 251 | 252 | yt.zipWriter = zip.NewWriter(file) 253 | 254 | chSig := make(chan os.Signal, 1) 255 | signal.Notify(chSig, os.Interrupt) 256 | go func() { 257 | <-chSig 258 | yt.mZip.Lock() 259 | defer yt.mZip.Unlock() 260 | if yt.zipWriter != nil { 261 | yt.zipWriter.Close() 262 | } 263 | os.Exit(0) 264 | }() 265 | return 266 | } 267 | 268 | func (yt *YtDash) WriteZip(name string, rdr io.Reader) (err error) { 269 | yt.mZip.Lock() 270 | defer yt.mZip.Unlock() 271 | 272 | if yt.zipFile == nil || yt.zipWriter == nil { 273 | yt.OpenFile() 274 | } 275 | 276 | wr, err := yt.zipWriter.Create(name) 277 | if err != nil { 278 | return 279 | } 280 | 281 | if _, err = io.Copy(wr, rdr); err != nil { 282 | return 283 | } 284 | return 285 | } 286 | func (yt *YtDash) RecordForward() { 287 | go func() { 288 | defer func() { 289 | close(yt.ChEnd) 290 | }() 291 | for { 292 | vfile, err := yt.fetchVideo() 293 | if err != nil { 294 | fmt.Printf("RecordForward: %v\n", err) 295 | return 296 | } 297 | afile, err := yt.fetchAudio() 298 | if err != nil { 299 | fmt.Printf("RecordForward: %v\n", err) 300 | return 301 | } 302 | if true { 303 | fmt.Printf("%s, %s\n", vfile, afile) 304 | } 305 | yt.IncrSeqNo() 306 | } 307 | }() 308 | } 309 | func (yt *YtDash) RecordBack() { 310 | if yt.TryBack && (! yt.StartBack) { 311 | yt.StartBack = true 312 | go func() { 313 | defer func() { 314 | close(yt.ChEndBack) 315 | }() 316 | for yt.SeqNoBack >= 0 { 317 | vfile, err := yt.fetchVideoBack() 318 | if err != nil { 319 | fmt.Printf("RecordBack: %v\n", err) 320 | return 321 | } 322 | afile, err := yt.fetchAudioBack() 323 | if err != nil { 324 | fmt.Printf("RecordBack: %v\n", err) 325 | return 326 | } 327 | if true { 328 | fmt.Printf("%s, %s\n", vfile, afile) 329 | } 330 | yt.DecrSeqNoBack() 331 | } 332 | }() 333 | } 334 | } 335 | 336 | func Record(id string) (err error) { 337 | uri := fmt.Sprintf("https://www.youtube.com/watch?v=%s", id) 338 | req, _ := http.NewRequest("GET", uri, nil) 339 | 340 | client := new(http.Client) 341 | resp, err := client.Do(req) 342 | if err != nil { 343 | fmt.Println(err) 344 | return 345 | } 346 | defer resp.Body.Close() 347 | dat, _ := ioutil.ReadAll(resp.Body) 348 | 349 | var a interface{} 350 | 351 | re := regexp.MustCompile(`\Wytplayer\.config\p{Zs}*=\p{Zs}*({.*?})\p{Zs}*;`) 352 | if ma := re.FindSubmatch(dat); len(ma) > 0 { 353 | str := html.UnescapeString(string(ma[1])) 354 | if err = json.Unmarshal([]byte(str), &a); err != nil { 355 | fmt.Println(str) 356 | fmt.Println(err) 357 | return 358 | } 359 | } else { 360 | fmt.Println("ytplayer.config not found") 361 | return 362 | } 363 | 364 | // debug print 365 | //obj.PrintAsJson(a) 366 | 367 | title, ok := obj.FindString(a, "args", "title") 368 | if (! ok) { 369 | fmt.Println("title not found") 370 | return 371 | } 372 | 373 | res, ok := obj.FindString(a, "args", "adaptive_fmts") 374 | if (! ok) { 375 | if res, ok := obj.FindString(a, "args", "hlsvp"); ok { 376 | fmt.Printf("hls: %s\n", res) 377 | return 378 | } 379 | obj.PrintAsJson(a) 380 | return 381 | } 382 | 383 | var maxVideoBr int 384 | var maxAudioBr int 385 | var videoUrl string 386 | var audioUrl string 387 | var qualityLabel string 388 | for _, s := range strings.Split(res, ",") { 389 | //fmt.Println(s) 390 | f, e := url.ParseQuery(s) 391 | //obj.PrintAsJson(f) 392 | //fmt.Println(f) 393 | if e != nil { 394 | fmt.Println(e) 395 | return 396 | } 397 | // type 398 | // bitrate 399 | t := f.Get("type") 400 | br, err := strconv.Atoi(f.Get("bitrate")) 401 | if err != nil { 402 | continue 403 | } 404 | 405 | if strings.HasPrefix(t, "video") { 406 | if br > maxVideoBr { 407 | maxVideoBr = br 408 | videoUrl = f.Get("url") 409 | qualityLabel = f.Get("quality_label") 410 | } 411 | } else if strings.HasPrefix(t, "audio") { 412 | if br > maxAudioBr { 413 | maxAudioBr = br 414 | audioUrl = f.Get("url") 415 | } 416 | } 417 | } 418 | fmt.Printf("Quality: %s\n", qualityLabel) 419 | 420 | varr := strings.SplitN(videoUrl, "?", 2) 421 | if len(varr) != 2 { 422 | return 423 | } 424 | aarr := strings.SplitN(audioUrl, "?", 2) 425 | if len(aarr) != 2 { 426 | return 427 | } 428 | 429 | yt := new(YtDash) 430 | defer yt.Close() 431 | 432 | yt.Id = id 433 | yt.Title = files.ReplaceForbidden(title) 434 | 435 | yt.SetFileName(fmt.Sprintf("%s(%s).zip", title, id)) 436 | 437 | yt.VAddr = varr[0] 438 | vQuery, e := url.ParseQuery(varr[1]) 439 | if e != nil { 440 | return 441 | } 442 | yt.VQuery = vQuery 443 | 444 | //obj.PrintAsJson(vQuery) 445 | //fmt.Println(yt.VAddr + "?" + vQuery.Encode()) 446 | 447 | yt.AAddr = aarr[0] 448 | aQuery, e := url.ParseQuery(aarr[1]) 449 | if e != nil { 450 | return 451 | } 452 | yt.AQuery = aQuery 453 | 454 | yt.TryBack = true 455 | yt.Wait() 456 | 457 | return 458 | } 459 | -------------------------------------------------------------------------------- /src/zip2mp4/zip2mp4.go: -------------------------------------------------------------------------------- 1 | package zip2mp4 2 | 3 | import ( 4 | "fmt" 5 | "archive/zip" 6 | "regexp" 7 | "strconv" 8 | "log" 9 | "sort" 10 | "io" 11 | "os" 12 | "os/exec" 13 | "io/ioutil" 14 | "../files" 15 | "../log4gui" 16 | 17 | "database/sql" 18 | _ "github.com/mattn/go-sqlite3" 19 | "bytes" 20 | "time" 21 | "../niconico" 22 | "../youtube" 23 | "../procs/ffmpeg" 24 | 25 | "context" 26 | "github.com/gin-gonic/gin" 27 | "net/http" 28 | ) 29 | 30 | type ZipMp4 struct { 31 | ZipName string 32 | Mp4NameOpened string 33 | mp4List []string 34 | 35 | FFMpeg *exec.Cmd 36 | FFStdin io.WriteCloser 37 | } 38 | var cmdListFF = []string{ 39 | "./bin/ffmpeg/ffmpeg", 40 | "./bin/ffmpeg", 41 | "./ffmpeg/ffmpeg", 42 | "./ffmpeg", 43 | "ffmpeg", 44 | } 45 | var cmdListMP42TS = []string{ 46 | "./bin/bento4/bin/mp42ts", 47 | "./bento4/bin/mp42ts", 48 | "./bento4/mp42ts", 49 | "./bin/bento4/mp42ts", 50 | "./bin/mp42ts", 51 | "./mp42ts", 52 | "mp42ts", 53 | } 54 | // return cmd = nil if cmd not exists 55 | func openProg(cmdList *[]string, stdinEn, stdoutEn, stdErrEn, consoleEn bool, args []string) (cmd *exec.Cmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) { 56 | 57 | for i, cmdName := range *cmdList { 58 | cmd = exec.Command(cmdName, args...) 59 | 60 | var err error 61 | if stdinEn { 62 | stdin, err = cmd.StdinPipe() 63 | if err != nil { 64 | log.Fatalln(err) 65 | } 66 | } 67 | 68 | if stdoutEn { 69 | stdout, err = cmd.StdoutPipe() 70 | if err != nil { 71 | log.Fatalln(err) 72 | } 73 | } else { 74 | if consoleEn { 75 | cmd.Stdout = os.Stdout 76 | } 77 | } 78 | 79 | if stdErrEn { 80 | stderr, err = cmd.StderrPipe() 81 | if err != nil { 82 | log.Fatalln(err) 83 | } 84 | } else { 85 | if consoleEn { 86 | cmd.Stderr = os.Stderr 87 | } 88 | } 89 | 90 | if err = cmd.Start(); err != nil { 91 | continue 92 | } else { 93 | if i != 0 { 94 | *cmdList = []string{cmdName} 95 | } 96 | //fmt.Printf("CMD: %#v\n", cmd.Args) 97 | return 98 | } 99 | } 100 | cmd = nil 101 | return 102 | } 103 | func MergeVA(vFileName, aFileName, oFileName string) bool { 104 | cmd, _, _, _ := openProg(&cmdListFF, false, false, false, true, []string{ 105 | "-i", vFileName, 106 | "-i", aFileName, 107 | "-c", "copy", 108 | "-y", 109 | oFileName, 110 | }) 111 | if cmd == nil { 112 | return false 113 | } 114 | if err := cmd.Wait(); err != nil { 115 | fmt.Println(err) 116 | return false 117 | } 118 | return true 119 | } 120 | func FFmpegExists() bool { 121 | cmd, _, _, _ := openProg(&cmdListFF, false, false, false, false, []string{"-version"}) 122 | if cmd == nil { 123 | return false 124 | } 125 | cmd.Wait() 126 | return true 127 | } 128 | func GetFormat(fileName string) (vFormat, aFormat string) { 129 | cmd, _, stdout, stderr := openProg(&cmdListFF, false, true, true, false, []string{"-i", fileName}) 130 | if cmd == nil { 131 | return 132 | } 133 | b1, _ := ioutil.ReadAll(stdout) 134 | b2, _ := ioutil.ReadAll(stderr) 135 | cmd.Wait() 136 | 137 | s := string(b1) + string(b2) 138 | if ma := regexp.MustCompile(`(?i)Stream\s+#.+?:\s+Video:\s+(.*?),`).FindStringSubmatch(s); len(ma) > 0 { 139 | vFormat = ma[1] 140 | } 141 | if ma := regexp.MustCompile(`(?i)Stream\s+#.+?:\s+Audio:\s+(.*?),`).FindStringSubmatch(s); len(ma) > 0 { 142 | aFormat = ma[1] 143 | } 144 | 145 | return 146 | } 147 | func openFFMpeg(stdinEn, stdoutEn, stdErrEn, consoleEn bool, args []string) (cmd *exec.Cmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser) { 148 | return openProg(&cmdListFF, stdinEn, stdoutEn, stdErrEn, consoleEn, args) 149 | } 150 | func openMP42TS(consoleEn bool, args []string) (cmd *exec.Cmd) { 151 | cmd, _, _, _ = openProg(&cmdListMP42TS, false, false, false, consoleEn, args) 152 | return 153 | } 154 | func (z *ZipMp4) Wait() { 155 | 156 | if z.FFStdin != nil { 157 | z.FFStdin.Close() 158 | } 159 | 160 | if z.FFMpeg != nil { 161 | if err := z.FFMpeg.Wait(); err != nil { 162 | log.Fatalln(err) 163 | } 164 | z.FFMpeg = nil 165 | } 166 | } 167 | func (z *ZipMp4) CloseFFInput() { 168 | z.FFStdin.Close() 169 | } 170 | func (z *ZipMp4) OpenFFMpeg(ext string) { 171 | // 172 | z.Wait() 173 | 174 | if ext == "" { 175 | ext = "mp4" 176 | } 177 | name := files.ChangeExtention(z.ZipName, ext) 178 | name, err := files.GetFileNameNext(name) 179 | if err != nil { 180 | fmt.Println(err) 181 | os.Exit(1) 182 | } 183 | z.Mp4NameOpened = name 184 | z.mp4List = append(z.mp4List, name) 185 | 186 | 187 | cmd, stdin, err := ffmpeg.Open( 188 | "-i", "-", 189 | "-c", "copy", 190 | //"-movflags", "faststart", // test 191 | "-y", 192 | name, 193 | ) 194 | if err != nil { 195 | log.Fatalln(err) 196 | } 197 | 198 | z.FFMpeg = cmd 199 | z.FFStdin = stdin 200 | } 201 | 202 | func (z *ZipMp4) FFInputCombFromFile(videoFile, audioFile string) { 203 | 204 | vTs := fmt.Sprintf("%s.ts", videoFile) 205 | cmdV := openMP42TS(false, []string{ 206 | videoFile, 207 | vTs, 208 | }) 209 | if cmdV == nil { 210 | fmt.Println("mp42ts not found OR command failed") 211 | os.Exit(1) 212 | } 213 | defer os.Remove(vTs) 214 | 215 | aTs := fmt.Sprintf("%s.ts", audioFile) 216 | cmdA := openMP42TS(false, []string{ 217 | audioFile, 218 | aTs, 219 | }) 220 | if cmdA == nil { 221 | fmt.Println("mp42ts not found OR command failed") 222 | os.Exit(1) 223 | } 224 | defer os.Remove(aTs) 225 | 226 | if err := cmdV.Wait(); err != nil { 227 | log.Fatalln(err) 228 | } 229 | if err := cmdA.Wait(); err != nil { 230 | log.Fatalln(err) 231 | } 232 | 233 | cmd, _, stdout, _ := openFFMpeg(false, true, false, false, []string{ 234 | "-i", vTs, 235 | "-i", aTs, 236 | "-c", "copy", 237 | "-f", "mpegts", 238 | "-", 239 | }) 240 | if cmd == nil { 241 | log.Fatalln("ffmpeg not installed") 242 | } 243 | 244 | z.FFInput(stdout) 245 | 246 | if err := cmd.Wait(); err != nil { 247 | log.Fatalln(err) 248 | } 249 | } 250 | func (z *ZipMp4) FFInput(rdr io.Reader) { 251 | if _, err := io.Copy(z.FFStdin, rdr); err != nil { 252 | log.Fatalln(err) 253 | } 254 | } 255 | 256 | type Index struct { 257 | int 258 | } 259 | type Chunk struct { 260 | VideoIndex *Index 261 | AudioIndex *Index 262 | VAIndex *Index 263 | } 264 | 265 | func Convert(fileName string) (err error) { 266 | zr, err := zip.OpenReader(fileName) 267 | if err != nil { 268 | return 269 | } 270 | 271 | chunks := make(map[int64]Chunk) 272 | 273 | for i, r := range zr.File { 274 | //fmt.Printf("X %v %v\n", i, r.Name) 275 | 276 | if ma := regexp.MustCompile(`\Avideo-(\d+)\.\w+\z`).FindStringSubmatch(r.Name); len(ma) > 0 { 277 | num, err := strconv.ParseInt(string(ma[1]), 10, 64) 278 | if err != nil { 279 | log.Fatal(err) 280 | } 281 | if v, ok := chunks[num]; ok { 282 | v.VideoIndex = &Index{i} 283 | chunks[num] = v 284 | } else { 285 | chunks[num] = Chunk{VideoIndex: &Index{i}} 286 | } 287 | 288 | //fmt.Printf("V %v %v\n", i, r.Name) 289 | } else if ma := regexp.MustCompile(`\Aaudio-(\d+)\.\w+\z`).FindStringSubmatch(r.Name); len(ma) > 0 { 290 | num, err := strconv.ParseInt(string(ma[1]), 10, 64) 291 | if err != nil { 292 | log.Fatal(err) 293 | } 294 | if v, ok := chunks[num]; ok { 295 | v.AudioIndex = &Index{i} 296 | chunks[num] = v 297 | } else { 298 | chunks[num] = Chunk{AudioIndex: &Index{i}} 299 | } 300 | //fmt.Printf("A %v %v\n", num, r.Name) 301 | } else if ma := regexp.MustCompile(`\A(\d+)\.\w+\z`).FindStringSubmatch(r.Name); len(ma) > 0 { 302 | num, err := strconv.ParseInt(string(ma[1]), 10, 64) 303 | if err != nil { 304 | log.Fatal(err) 305 | } 306 | if v, ok := chunks[num]; ok { 307 | v.VAIndex = &Index{i} 308 | chunks[num] = v 309 | } else { 310 | chunks[num] = Chunk{VAIndex: &Index{i}} 311 | } 312 | //fmt.Printf("V+A %v %v\n", num, r.Name) 313 | } else { 314 | fmt.Printf("%v %v\n", i, r.Name) 315 | log4gui.Info(fmt.Sprintf("Unsupported zip: %s", fileName)) 316 | os.Exit(1) 317 | } 318 | } 319 | 320 | keys := make([]int64, 0, len(chunks)) 321 | for k := range chunks { 322 | keys = append(keys, k) 323 | } 324 | 325 | sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) 326 | 327 | var tmpVideoName string 328 | var tmpAudioName string 329 | 330 | var zm *ZipMp4 331 | defer func() { 332 | if zm != nil { 333 | zm.CloseFFInput() 334 | zm.Wait() 335 | } 336 | }() 337 | 338 | zm = &ZipMp4{ZipName: fileName} 339 | zm.OpenFFMpeg("mp4") 340 | 341 | prevIndex := int64(-1) 342 | for _, key := range keys { 343 | if prevIndex >= 0 { 344 | if key != prevIndex + 1 { 345 | // [FIXME] reopen new mp4file? 346 | //return fmt.Errorf("\n\nError: seq skipped: %d --> %d\n\n", prevIndex, key) 347 | 348 | fmt.Printf("\nSeqNo. skipped: %d --> %d\n", prevIndex, key) 349 | if zm != nil { 350 | zm.CloseFFInput() 351 | zm.Wait() 352 | } 353 | zm = &ZipMp4{ZipName: fileName} 354 | zm.OpenFFMpeg("mp4") 355 | } 356 | } 357 | prevIndex = key 358 | 359 | if chunks[key].VAIndex != nil { 360 | r, e := zr.File[chunks[key].VAIndex.int].Open() 361 | if e != nil { 362 | log.Fatalln(e) 363 | } 364 | zm.FFInput(r) 365 | r.Close() 366 | 367 | } else if chunks[key].VideoIndex != nil && chunks[key].AudioIndex != nil { 368 | 369 | if tmpVideoName == "" { 370 | f, e := ioutil.TempFile(".", "__temp-") 371 | if e != nil { 372 | log.Fatalln(e) 373 | } 374 | f.Close() 375 | tmpVideoName = f.Name() 376 | } 377 | if tmpAudioName == "" { 378 | f, e := ioutil.TempFile(".", "__temp-") 379 | if e != nil { 380 | log.Fatalln(e) 381 | } 382 | f.Close() 383 | tmpAudioName = f.Name() 384 | } 385 | 386 | // open temporary file 387 | tmpVideo, err := os.Create(tmpVideoName) 388 | if err != nil { 389 | log.Fatalln(err) 390 | } 391 | tmpAudio, err := os.Create(tmpAudioName) 392 | if err != nil { 393 | log.Fatalln(err) 394 | } 395 | 396 | // copy Video to file 397 | rv, e := zr.File[chunks[key].VideoIndex.int].Open() 398 | if e != nil { 399 | log.Fatalln(e) 400 | } 401 | if _, e := io.Copy(tmpVideo, rv); e != nil { 402 | log.Fatalln(e) 403 | } 404 | rv.Close() 405 | tmpVideo.Close() 406 | 407 | // copy Audio to file 408 | ra, e := zr.File[chunks[key].AudioIndex.int].Open() 409 | if e != nil { 410 | log.Fatalln(e) 411 | } 412 | if _, e := io.Copy(tmpAudio, ra); e != nil { 413 | log.Fatalln(e) 414 | } 415 | ra.Close() 416 | tmpAudio.Close() 417 | 418 | // combine video + audio using ffmpeg(+mp42ts) 419 | zm.FFInputCombFromFile(tmpVideoName, tmpAudioName) 420 | os.Remove(tmpVideoName) 421 | os.Remove(tmpAudioName) 422 | } else { 423 | if (chunks[key].VideoIndex == nil && chunks[key].AudioIndex != nil) || 424 | (chunks[key].VideoIndex != nil && chunks[key].AudioIndex == nil) { 425 | fmt.Printf("\nIncomplete sequence. skipped: %d\n", key) 426 | if zm != nil { 427 | zm.CloseFFInput() 428 | zm.Wait() 429 | } 430 | zm = &ZipMp4{ZipName: fileName} 431 | zm.OpenFFMpeg("mp4") 432 | } 433 | } 434 | } 435 | 436 | zm.CloseFFInput() 437 | zm.Wait() 438 | fmt.Printf("\nfinish: %s\n", zm.Mp4NameOpened) 439 | 440 | return 441 | } 442 | 443 | 444 | func ExtractChunks(fileName string, skipHb bool) (done bool, err error) { 445 | db, err := sql.Open("sqlite3", fileName) 446 | if err != nil { 447 | return 448 | } 449 | defer db.Close() 450 | 451 | niconico.WriteComment(db, fileName, skipHb) 452 | 453 | rows, err := db.Query(niconico.SelMedia) 454 | if err != nil { 455 | return 456 | } 457 | defer rows.Close() 458 | 459 | dir := files.RemoveExtention(fileName) 460 | if err = files.MkdirByFileName(dir + "/"); err != nil { 461 | return 462 | } 463 | var printTime int64 464 | for rows.Next() { 465 | var seqno int64 466 | var bw int 467 | var size int 468 | var data []byte 469 | err = rows.Scan(&seqno, &bw, &size, &data) 470 | if err != nil { 471 | return 472 | } 473 | name := fmt.Sprintf("%s/%d.ts", dir, seqno) 474 | // print 475 | now := time.Now().Unix() 476 | if now != printTime { 477 | printTime = now 478 | fmt.Println(name) 479 | } 480 | 481 | err = func() (e error) { 482 | f, e := os.Create(name) 483 | if e != nil { 484 | return 485 | } 486 | defer f.Close() 487 | _, e = f.Write(data) 488 | return 489 | }() 490 | if err != nil { 491 | return 492 | } 493 | } 494 | 495 | done = true 496 | return 497 | } 498 | 499 | func ConvertDB(fileName, ext string, skipHb, forceConcat bool, seqnoStart, seqnoEnd int64) (done bool, nMp4s int, skipped bool, err error) { 500 | db, err := sql.Open("sqlite3", fileName) 501 | if err != nil { 502 | return 503 | } 504 | defer db.Close() 505 | 506 | niconico.WriteComment(db, fileName, skipHb) 507 | 508 | var zm *ZipMp4 509 | defer func() { 510 | if zm != nil { 511 | //zm.CloseFFInput() 512 | zm.Wait() 513 | } 514 | }() 515 | 516 | zm = &ZipMp4{ZipName: fileName} 517 | zm.OpenFFMpeg(ext) 518 | 519 | rows, err := db.Query(niconico.SelMediaF(seqnoStart, seqnoEnd)) 520 | if err != nil { 521 | return 522 | } 523 | defer rows.Close() 524 | 525 | prevBw := -1 526 | prevIndex := int64(-1) 527 | for rows.Next() { 528 | var seqno int64 529 | var bw int 530 | var size int 531 | var data []byte 532 | err = rows.Scan(&seqno, &bw, &size, &data) 533 | if err != nil { 534 | return 535 | } 536 | 537 | // チャンクが飛んでいる場合はファイルを分ける 538 | // BANDWIDTHが変わる場合はファイルを分ける 539 | if (prevIndex >= 0 && seqno != prevIndex + 1) || (prevBw >= 0 && bw != prevBw) { 540 | if bw != prevBw { 541 | fmt.Printf("\nBandwitdh changed: %d --> %d\n\n", prevBw, bw) 542 | } else { 543 | fmt.Printf("\nSeqNo. skipped: %d --> %d\n\n", prevIndex, seqno) 544 | } 545 | 546 | //if zm != nil { 547 | // zm.CloseFFInput() 548 | // zm.Wait() 549 | //} 550 | if ! forceConcat { 551 | zm.OpenFFMpeg(ext) 552 | } 553 | skipped = true 554 | } 555 | prevBw = bw 556 | prevIndex = seqno 557 | 558 | zm.FFInput(bytes.NewBuffer(data)) 559 | } 560 | 561 | //zm.CloseFFInput() 562 | zm.Wait() 563 | fmt.Printf("\nfinish:\n") 564 | for _, s := range zm.mp4List { 565 | fmt.Println(s) 566 | } 567 | done = true 568 | nMp4s = len(zm.mp4List) 569 | 570 | return 571 | } 572 | 573 | func ReplayDB(fileName string, hlsPort int, seqnoStart int64) (err error) { 574 | db, err := sql.Open("sqlite3", fileName) 575 | if err != nil { 576 | return 577 | } 578 | defer db.Close() 579 | 580 | var isTimeshift bool 581 | if m := regexp.MustCompile(`\(TS\)\.sqlite3$`).FindStringSubmatch(fileName); len(m) > 0 { 582 | isTimeshift = true 583 | } 584 | fmt.Println("isTimeshift:", isTimeshift) 585 | 586 | seqnoInit := seqnoStart 587 | 588 | timeStart := time.Now() 589 | timeLast := time.Now() 590 | 591 | seqnoCurrent := seqnoStart 592 | 593 | if (true) { 594 | gin.SetMode(gin.ReleaseMode) 595 | gin.DefaultErrorWriter = ioutil.Discard 596 | gin.DefaultWriter = ioutil.Discard 597 | router := gin.Default() 598 | 599 | router.GET("", func(c *gin.Context) { 600 | c.Redirect(http.StatusMovedPermanently, "/m3u8/2/0/index.m3u8") 601 | c.Abort() 602 | }) 603 | 604 | router.GET("/m3u8/:delay/:shift/index.m3u8", func(c *gin.Context) { 605 | secPerSegment := 1.5 606 | targetDuration := "2" 607 | targetDurationFloat := 2.0 608 | extInf := "1.5" 609 | if isTimeshift { 610 | secPerSegment = 5.0 611 | targetDuration = "3" 612 | targetDurationFloat = 3.0 613 | extInf = "3.0" 614 | } 615 | shift, err := strconv.Atoi(c.Param("shift")) 616 | if err != nil { 617 | shift = 0 618 | } 619 | if shift < 0 { 620 | shift = 0 621 | } 622 | delay, err := strconv.Atoi(c.Param("delay")) 623 | if err != nil { 624 | delay = 0 625 | } 626 | if delay < 2 { 627 | delay = 2 628 | } 629 | if (! isTimeshift) { 630 | if delay < 4 { 631 | delay = 4 632 | } 633 | } 634 | seqnoRewind := int64(delay) 635 | timeout := targetDurationFloat * float64(delay + 1) * 2 + 1 636 | timeNow := time.Now() 637 | if float64(timeNow.Sub(timeLast) / time.Second) > timeout { 638 | fmt.Printf("(%s) CONTINUE\n", timeNow.Format("15:04:05")) 639 | seqnoStart = seqnoCurrent - seqnoRewind 640 | if seqnoStart < seqnoInit { 641 | seqnoStart = seqnoInit 642 | } 643 | timeStart = timeNow 644 | seqnoCurrent = seqnoStart 645 | } else { 646 | seqnoCurrent = int64(float64(timeNow.Sub(timeStart) / time.Second) / secPerSegment) + seqnoStart 647 | } 648 | timeLast = timeNow 649 | seqno := seqnoCurrent - int64(shift) 650 | body := fmt.Sprintf( 651 | `#EXTM3U 652 | #EXT-X-VERSION:3 653 | #EXT-X-TARGETDURATION:%s 654 | #EXT-X-MEDIA-SEQUENCE:%d 655 | 656 | `, targetDuration, seqno) 657 | for i := int64(delay); i >= 0; i-- { 658 | body += fmt.Sprintf( 659 | `#EXTINF:%s, 660 | /ts/%d/test.ts 661 | 662 | `, extInf, seqno - i) 663 | } 664 | if shift > 0 { 665 | fmt.Printf("(%s) Current SeqNo: %d(-%d)\n", timeNow.Format("15:04:05"), seqnoCurrent, shift) 666 | } else { 667 | fmt.Printf("(%s) Current SeqNo: %d\n", timeNow.Format("15:04:05"), seqnoCurrent) 668 | } 669 | c.Data(http.StatusOK, "application/x-mpegURL", []byte(body)) 670 | return 671 | }) 672 | 673 | router.GET("/ts/:idx/test.ts", func(c *gin.Context) { 674 | i, _ := strconv.Atoi(c.Param("idx")) 675 | var b []byte 676 | db.QueryRow("SELECT data FROM media WHERE seqno = ?", i).Scan(&b) 677 | c.Data(http.StatusOK, "video/MP2T", b) 678 | return 679 | }) 680 | 681 | srv := &http.Server{ 682 | Addr: fmt.Sprintf("127.0.0.1:%d", hlsPort), 683 | Handler: router, 684 | ReadTimeout: 10 * time.Second, 685 | WriteTimeout: 10 * time.Second, 686 | MaxHeaderBytes: 1 << 20, 687 | } 688 | 689 | chLocal := make(chan struct{}) 690 | idleConnsClosed := make(chan struct{}) 691 | defer func(){ 692 | close(chLocal) 693 | }() 694 | go func() { 695 | select { 696 | case <-chLocal: 697 | } 698 | if err := srv.Shutdown(context.Background()); err != nil { 699 | log.Printf("srv.Shutdown: %v\n", err) 700 | } 701 | close(idleConnsClosed) 702 | }() 703 | 704 | // クライアントはlocalhostでなく127.0.0.1で接続すること 705 | // localhostは遅いため 706 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 707 | log.Printf("srv.ListenAndServe: %v\n", err) 708 | } 709 | 710 | <-idleConnsClosed 711 | } 712 | 713 | return 714 | } 715 | 716 | func YtComment(fileName string) (done bool, err error) { 717 | db, err := sql.Open("sqlite3", fileName) 718 | if err != nil { 719 | return 720 | } 721 | defer db.Close() 722 | 723 | youtube.WriteComment(db, fileName) 724 | return 725 | } 726 | -------------------------------------------------------------------------------- /updatebuildno.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "io/ioutil" 7 | "time" 8 | "strconv" 9 | "regexp" 10 | "fmt" 11 | ) 12 | 13 | func main() { 14 | f, err := os.OpenFile("src/buildno/buildno.go", os.O_RDWR, 0755) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | defer f.Close() 19 | 20 | if _, err := f.Seek(0, 0); err != nil { 21 | log.Fatal(err) 22 | } 23 | data, err := ioutil.ReadAll(f) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | var buildNo int64 29 | if ma := regexp.MustCompile(`BuildNo\s*=\s*"(\d+)"`).FindSubmatch(data); len(ma) > 0 { 30 | buildNo, err = strconv.ParseInt(string(ma[1]), 10, 64) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | } else { 35 | log.Fatal("BuildNo not match") 36 | } 37 | buildNo++ 38 | 39 | var now = time.Now() 40 | buildDate := fmt.Sprintf("%04d%02d%02d", 41 | now.Year(), 42 | now.Month(), 43 | now.Day(), 44 | ) 45 | 46 | fmt.Printf("%v.%v\n", buildDate, buildNo) 47 | 48 | if _, err := f.Seek(0, 0); err != nil { 49 | log.Fatal(err) 50 | } 51 | if err := f.Truncate(0); err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | f.WriteString(fmt.Sprintf(` 56 | package buildno 57 | 58 | var BuildDate = "%s" 59 | var BuildNo = "%d" 60 | `, buildDate, buildNo)) 61 | 62 | } 63 | --------------------------------------------------------------------------------