diff --git a/client.go b/client.go index 30f92a5c..3c7198d0 100644 --- a/client.go +++ b/client.go @@ -29,7 +29,7 @@ var ( ) // DefaultClient type to use. No reason to change but you could if you wanted to. -var DefaultClient = AndroidClient +var DefaultClient = IosClient // Client offers methods to download video metadata and video streams. type Client struct { @@ -72,6 +72,16 @@ func (c *Client) GetVideoContext(ctx context.Context, url string) (*Video, error return c.videoFromID(ctx, id) } +func (c *Client) fetchHLS(ctx context.Context, v *Video) error { + resp, err := c.httpGet(ctx, v.HLSManifestURL) + if err != nil { + return err + } + defer resp.Body.Close() + parseM3U8(resp.Body) + return nil +} + func (c *Client) videoFromID(ctx context.Context, id string) (*Video, error) { c.assureClient() @@ -86,6 +96,11 @@ func (c *Client) videoFromID(ctx context.Context, id string) (*Video, error) { // return early if all good if err = v.parseVideoInfo(body); err == nil { + + if v.HLSManifestURL != "" { + c.fetchHLS(ctx, &v) + } + return &v, nil } diff --git a/cmd/youtubedr/info.go b/cmd/youtubedr/info.go index 0bf83608..65c73a14 100644 --- a/cmd/youtubedr/info.go +++ b/cmd/youtubedr/info.go @@ -3,6 +3,7 @@ package main import ( "fmt" "io" + "log" "os" "strconv" "strings" @@ -52,6 +53,9 @@ var infoCmd = &cobra.Command{ Description: video.Description, } + log.Println("DASH", video.DASHManifestURL) + log.Println("HLS", video.HLSManifestURL) + for _, format := range video.Formats { bitrate := format.AverageBitrate if bitrate == 0 { diff --git a/m3u8.go b/m3u8.go new file mode 100644 index 00000000..f4d1384f --- /dev/null +++ b/m3u8.go @@ -0,0 +1,49 @@ +package youtube + +import ( + "bufio" + "io" + "strconv" + "strings" +) + +func parseM3U8(r io.Reader) ([]Format, error) { + var result []Format + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "#EXT-X-STREAM-INF:") { + continue + } + + // TODO parse line + scanner.Scan() + url := scanner.Text() + + itag := extractURLcomponent(url, "itag") + itagNo, _ := strconv.Atoi(itag) + + result = append(result, Format{ + URL: url, + ItagNo: itagNo, + }) + } + + return result, scanner.Err() +} + +func extractURLcomponent(url, arg string) string { + i := strings.Index(url, "/"+arg+"/") + if i < 0 { + return "" + } + i += len(arg) + 2 + + j := strings.Index(url[i:], "/") + if j < 0 { + return "" + } + + return url[i : i+j] +} diff --git a/m3u8_test.go b/m3u8_test.go new file mode 100644 index 00000000..136378d4 --- /dev/null +++ b/m3u8_test.go @@ -0,0 +1,24 @@ +package youtube + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseM3U8(t *testing.T) { + assert, require := assert.New(t), require.New(t) + file, err := os.Open("testdata/index.m3u8") + + require.NoError(err) + assert.NotNil(file) + defer file.Close() + + result, err := parseM3U8(file) + require.NoError(err) + require.Len(result, 4) + + assert.Equal(229, result[0].ItagNo) +} diff --git a/testdata/index.m3u8 b/testdata/index.m3u8 new file mode 100644 index 00000000..2ee0e851 --- /dev/null +++ b/testdata/index.m3u8 @@ -0,0 +1,12 @@ +#EXTM3U +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MEDIA:URI="https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1703490098/ei/0t2IZaThF-y86dsP5rKOsAU/ip/2a00:c381:e005:70d0:cda6:d594:d8d2:314f/id/61c35ce0804b2350/itag/233/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/goi/133/sgoap/clen%3D32217304%3Bdur%3D5283.282%3Bgir%3Dyes%3Bitag%3D139%3Blmt%3D1701065390462815/rqh/1/hls_chunk_host/rr1---sn-i5heen7z.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/Id/mm/31,29/mn/sn-i5heen7z,sn-i5h7lner/ms/au,rdu/mv/m/mvi/1/pl/36/force_finished/1/initcwndbps/2698750/vprv/1/playlist_type/DVR/dover/13/txp/5532434/mt/1703468463/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,goi,sgoap,rqh,xpc,force_finished,vprv,playlist_type/sig/AJfQdSswRAIgIaXunck2_oD6xkLrrU_PoLCVTCM3In8aNsSAMRyyiSsCIF1F_AVVSunFKh6-GNAqXzpdBLam9RC3fYe1jPQTqLK4/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AAO5W4owRQIgNWI34JJquq1UJiquWRQvAFcUrwxScaipZWCLD05Kdf8CIQCIrmAnf5jjZXYm7JjMRMc3hNXH07h1PZYBPfTvCeoK3A%3D%3D/playlist/index.m3u8",TYPE=AUDIO,GROUP-ID="233",NAME="Default",DEFAULT=YES,AUTOSELECT=YES +#EXT-X-MEDIA:URI="https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1703490098/ei/0t2IZaThF-y86dsP5rKOsAU/ip/2a00:c381:e005:70d0:cda6:d594:d8d2:314f/id/61c35ce0804b2350/itag/234/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/goi/133/sgoap/clen%3D85504044%3Bdur%3D5283.236%3Bgir%3Dyes%3Bitag%3D140%3Blmt%3D1701065410347038/rqh/1/hls_chunk_host/rr1---sn-i5heen7z.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/Id/mm/31,29/mn/sn-i5heen7z,sn-i5h7lner/ms/au,rdu/mv/m/mvi/1/pl/36/force_finished/1/initcwndbps/2698750/vprv/1/playlist_type/DVR/dover/13/txp/5532434/mt/1703468463/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,goi,sgoap,rqh,xpc,force_finished,vprv,playlist_type/sig/AJfQdSswRAIgWnY3H7EEwncXleUiDGxk7HMDsKPRx_fge5fFiTgSIXICIG0ah2ooN1H5X1mJCFAlZ7O_Z5yu9icMl0ga6pMfx8Vp/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AAO5W4owRgIhANVlq74IorMvHKbDnylPtxMyRxnffDxza1Yn132TQmHPAiEA3L6LAO-T_XtyuULq_RIoZi7kWuzZmTQ9BpxUtAGJ5yg%3D/playlist/index.m3u8",TYPE=AUDIO,GROUP-ID="234",NAME="Default",DEFAULT=YES,AUTOSELECT=YES +#EXT-X-STREAM-INF:BANDWIDTH=294157,CODECS="avc1.4D4015,mp4a.40.5",RESOLUTION=426x240,FRAME-RATE=25,VIDEO-RANGE=SDR,AUDIO="233",CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1703490098/ei/0t2IZaThF-y86dsP5rKOsAU/ip/2a00:c381:e005:70d0:cda6:d594:d8d2:314f/id/61c35ce0804b2350/itag/229/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D32957304%3Bdur%3D5283.160%3Bgir%3Dyes%3Bitag%3D133%3Blmt%3D1701071526311950/rqh/1/hls_chunk_host/rr1---sn-i5heen7z.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/Id/mm/31,29/mn/sn-i5heen7z,sn-i5h7lner/ms/au,rdu/mv/m/mvi/1/pl/36/force_finished/1/initcwndbps/2698750/vprv/1/playlist_type/DVR/dover/13/txp/5535434/mt/1703468463/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,rqh,xpc,force_finished,vprv,playlist_type/sig/AJfQdSswRgIhAJZhHGpPpIrcBpogYNT44NGD7zBu9lJE8OKAPDvD_bPlAiEA2HVtye2wAy8otXj2nfwZzQtp5gdRc7Jid-XSIwx2r7g%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AAO5W4owRQIgCC4_JiQPqoUs26SVZXU3NL6LfBTIwt8VLCE6vSrKiIECIQDy3h5XYWUlpsp6gzq05xpZEStl-nglRpxnmXqrTvvUPw%3D%3D/playlist/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=1747396,CODECS="vp09.00.31.08,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=25,VIDEO-RANGE=SDR,AUDIO="234",CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1703490098/ei/0t2IZaThF-y86dsP5rKOsAU/ip/2a00:c381:e005:70d0:cda6:d594:d8d2:314f/id/61c35ce0804b2350/itag/609/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D164286710%3Bdur%3D5283.160%3Bgir%3Dyes%3Bitag%3D247%3Blmt%3D1701069221015789/rqh/1/hls_chunk_host/rr1---sn-i5heen7z.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/Id/mm/31,29/mn/sn-i5heen7z,sn-i5h7lner/ms/au,rdu/mv/m/mvi/1/pl/36/force_finished/1/initcwndbps/2698750/vprv/1/playlist_type/DVR/dover/13/txp/5535434/mt/1703468463/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,rqh,xpc,force_finished,vprv,playlist_type/sig/AJfQdSswRQIhAKTf9inutX7RxcUshYgqGRj_MO5VRoVxydZw36yh9pDCAiB7sj7YbGesIzOca7j9julETmasbWclor_9BUc_rWdlGA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AAO5W4owRAIgSfVltd6kS_haQE23UiGZ1KibxZCT-VU1PbQi9gxHCB0CIB-a_ZGP-9pMYcgy7Na6h46SMYERy2JziSs9YrpYmv_8/playlist/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=2803988,CODECS="vp09.00.40.08,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25,VIDEO-RANGE=SDR,AUDIO="234",CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1703490098/ei/0t2IZaThF-y86dsP5rKOsAU/ip/2a00:c381:e005:70d0:cda6:d594:d8d2:314f/id/61c35ce0804b2350/itag/614/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D410468741%3Bdur%3D5283.160%3Bgir%3Dyes%3Bitag%3D248%3Blmt%3D1701068841070619/rqh/1/hls_chunk_host/rr1---sn-i5heen7z.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/Id/mm/31,29/mn/sn-i5heen7z,sn-i5h7lner/ms/au,rdu/mv/m/mvi/1/pl/36/force_finished/1/initcwndbps/2698750/vprv/1/playlist_type/DVR/dover/13/txp/5535434/mt/1703468463/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,rqh,xpc,force_finished,vprv,playlist_type/sig/AJfQdSswRAIgNXg1rfixd-GX-XmjwU6-2VKQSxz_WxW5zj-CZUy6BS0CIDbmmXIG2_mZtla0rIk1MONPM5r5TMtbkGrqkTsgd-jM/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AAO5W4owRgIhAPQXbJtXeax1ibFM5bJMcjxrgp3iy7qt7toTX82slXZ1AiEA-j8zoJ-CG78PrbZkvBWh8tBHimIg5xRr2tiJFDpfvmE%3D/playlist/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=5777838,CODECS="vp09.00.40.08,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=25,VIDEO-RANGE=SDR,AUDIO="234",CLOSED-CAPTIONS=NONE +https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1703490098/ei/0t2IZaThF-y86dsP5rKOsAU/ip/2a00:c381:e005:70d0:cda6:d594:d8d2:314f/id/61c35ce0804b2350/itag/616/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D1064670006%3Bdur%3D5283.160%3Bgir%3Dyes%3Bitag%3D356%3Blmt%3D1701065764877554/rqh/1/hls_chunk_host/rr1---sn-i5heen7z.googlevideo.com/xpc/EgVo2aDSNQ%3D%3D/mh/Id/mm/31,29/mn/sn-i5heen7z,sn-i5h7lner/ms/au,rdu/mv/m/mvi/1/pl/36/force_finished/1/initcwndbps/2698750/vprv/1/playlist_type/DVR/dover/13/txp/5532434/mt/1703468463/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,rqh,xpc,force_finished,vprv,playlist_type/sig/AJfQdSswRQIhAKm06ZrEroaCGoSx9trZEQi7G6ZoBI7-kPLEZO2TCdLzAiBqqORueVMBFRRWDu-1f9NBpS-ltCAlwbpnzl7iGkazPA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AAO5W4owRQIgJOchZn1jYj1SOMpghtNkpLB3pvPaPWE8DZN4kl5CWqICIQD6Q-zYM1e8_FKx000B_xAj8EmJ6ZiEggAa1aj8otpz2Q%3D%3D/playlist/index.m3u8