add atmos and select to main.go

--atmos,--select
pull/23/merge
zhaarey 2 months ago
parent af1a310cb6
commit c1c62ab94c
  1. 519
      main.go

@ -37,6 +37,8 @@ const (
var (
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
)
var dl_atmos = false
var dl_select = false
type Config struct {
MediaUserToken string `yaml:"media-user-token"`
@ -63,6 +65,7 @@ type Config struct {
GetM3u8Port string `yaml:"get-m3u8-port"`
GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"`
AlacMax int `yaml:"alac-max"`
AtmosMax int `yaml:"atmos-max"`
UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"`
DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"`
}
@ -109,6 +112,15 @@ func (*Alac) GetType() mp4.BoxType {
return BoxTypeAlac()
}
func isInArray(arr []int, target int) bool {
for _, num := range arr {
if num == target {
return true
}
}
return false
}
func fileExists(path string) (bool, error) {
f, err := os.Stat(path)
if err == nil {
@ -1009,6 +1021,14 @@ func decryptSong(info *SongInfo, keys []string, manifest *AutoGenerated, filenam
return err
}
defer create.Close()
if dl_atmos {
_, err = create.Write(decrypted)
if err != nil {
panic(err)
}
return nil
}
return writeM4a(mp4.NewWriter(create), info, manifest, decrypted, trackNum, trackTotal)
}
@ -1304,6 +1324,12 @@ func writeLyrics(sanAlbumFolder, filename string, lrc string) error {
}
func rip(albumId string, token string, storefront string, userToken string) error {
var Codec string
if dl_atmos {
Codec = "Atmos"
} else {
Codec = "ALAC"
}
meta, err := getMeta(albumId, token, storefront)
if err != nil {
fmt.Println("Failed to get album metadata.\n")
@ -1336,20 +1362,24 @@ func rip(albumId string, token string, storefront string, userToken string) erro
singerFolder := filepath.Join(config.AlacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
var Quality string
if strings.Contains(config.AlbumFolderFormat, "Quality") {
manifest1, err := getInfoFromAdam(meta.Data[0].Relationships.Tracks.Data[0].ID, token, storefront)
if err != nil {
fmt.Println("Failed to get manifest.\n", err)
if dl_atmos {
Quality = fmt.Sprintf("%dkbps", config.AtmosMax-2000)
} else {
if manifest1.Attributes.ExtendedAssetUrls.EnhancedHls == "" {
fmt.Println("Unavailable in ALAC.\n")
manifest1, err := getInfoFromAdam(meta.Data[0].Relationships.Tracks.Data[0].ID, token, storefront)
if err != nil {
fmt.Println("Failed to get manifest.\n", err)
} else {
EnhancedHls_m3u8, err := checkM3u8(meta.Data[0].Relationships.Tracks.Data[0].ID, "album")
if strings.HasPrefix(EnhancedHls_m3u8, "http") {
manifest1.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8
}
Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err)
if manifest1.Attributes.ExtendedAssetUrls.EnhancedHls == "" {
fmt.Println("Unavailable.\n")
} else {
EnhancedHls_m3u8, err := checkM3u8(meta.Data[0].Relationships.Tracks.Data[0].ID, "album")
if strings.HasPrefix(EnhancedHls_m3u8, "http") {
manifest1.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8
}
Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err)
}
}
}
}
@ -1378,7 +1408,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
"{PlaylistName}", meta.Data[0].Attributes.Name,
"{PlaylistId}", albumId,
"{Quality}", Quality,
"{Codec}", "ALAC",
"{Codec}", Codec,
"{Tag}", Tag_string,
).Replace(config.PlaylistFolderFormat)
} else {
@ -1392,7 +1422,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
"{Copyright}", meta.Data[0].Attributes.Copyright,
"{AlbumId}", albumId,
"{Quality}", Quality,
"{Codec}", "ALAC",
"{Codec}", Codec,
"{Tag}", Tag_string,
).Replace(config.AlbumFolderFormat)
}
@ -1448,157 +1478,260 @@ func rip(albumId string, token string, storefront string, userToken string) erro
}
}
trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
trackNum++
trackTotalnum += 1
fmt.Printf("Track %d of %d:\n", trackNum, trackTotal)
manifest, err := getInfoFromAdam(track.ID, token, storefront)
arr := make([]int, trackTotal)
for i := 0; i < trackTotal; i++ {
arr[i] = i + 1
}
selected := []int{}
if !dl_select {
selected = arr
} else {
fmt.Print("select: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Failed to get manifest.\n", err)
continue
}
if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" {
fmt.Println("Unavailable in ALAC.")
continue
}
EnhancedHls_m3u8, err := checkM3u8(track.ID, "song")
if strings.HasPrefix(EnhancedHls_m3u8, "http") {
manifest.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8
fmt.Println(err)
}
var Quality string
if strings.Contains(config.SongFileFormat, "Quality") {
Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
input = strings.TrimSpace(input)
inputs := strings.Fields(input)
for _, str := range inputs {
num, err := strconv.Atoi(str)
if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err)
fmt.Printf("wrong '%s', skip...\n", str)
continue
}
}
stringsToJoin := []string{}
if track.Attributes.IsAppleDigitalMaster {
if config.AppleMasterChoice != "" {
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
}
}
if track.Attributes.ContentRating == "explicit" {
if config.ExplicitChoice != "" {
stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
found := false
for i := 0; i < len(arr); i++ {
if arr[i] == num {
selected = append(selected, num)
found = true
break
}
}
}
if track.Attributes.ContentRating == "clean" {
if config.CleanChoice != "" {
stringsToJoin = append(stringsToJoin, config.CleanChoice)
if !found {
fmt.Printf("Option '%d' not found or already selected, skipping...\n", num)
}
}
Tag_string := strings.Join(stringsToJoin, " ")
songName := strings.NewReplacer(
"{SongId}", track.ID,
"{SongNumer}", fmt.Sprintf("%02d", trackNum),
"{SongName}", track.Attributes.Name,
"{DiscNumber}", fmt.Sprintf("%0d", track.Attributes.DiscNumber),
"{TrackNumber}", fmt.Sprintf("%0d", track.Attributes.TrackNumber),
"{Quality}", Quality,
"{Tag}", Tag_string,
"{Codec}", "ALAC",
).Replace(config.SongFileFormat)
fmt.Println(songName)
filename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_"))
lrcFilename := fmt.Sprintf("%s.lrc", forbiddenNames.ReplaceAllString(songName, "_"))
trackPath := filepath.Join(sanAlbumFolder, filename)
var lrc string = ""
if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) {
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
fmt.Println("Selected options:", selected)
}
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
trackNum++
if isInArray(selected, trackNum) {
trackTotalnum += 1
fmt.Printf("Track %d of %d:\n", trackNum, trackTotal)
manifest, err := getInfoFromAdam(track.ID, token, storefront)
if err != nil {
fmt.Println("Failed to get lyrics")
} else {
lrc, err = conventTTMLToLRC(ttml)
fmt.Println("Failed to get manifest.\n", err)
continue
}
if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" {
fmt.Println("Unavailable.")
continue
}
EnhancedHls_m3u8, err := checkM3u8(track.ID, "song")
if strings.HasPrefix(EnhancedHls_m3u8, "http") {
manifest.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8
}
var Quality string
if strings.Contains(config.SongFileFormat, "Quality") {
if dl_atmos {
Quality = fmt.Sprintf("%dkbps", config.AtmosMax-2000)
} else {
Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err)
continue
}
}
}
stringsToJoin := []string{}
if track.Attributes.IsAppleDigitalMaster {
if config.AppleMasterChoice != "" {
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
}
}
if track.Attributes.ContentRating == "explicit" {
if config.ExplicitChoice != "" {
stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
}
}
if track.Attributes.ContentRating == "clean" {
if config.CleanChoice != "" {
stringsToJoin = append(stringsToJoin, config.CleanChoice)
}
}
Tag_string := strings.Join(stringsToJoin, " ")
songName := strings.NewReplacer(
"{SongId}", track.ID,
"{SongNumer}", fmt.Sprintf("%02d", trackNum),
"{SongName}", track.Attributes.Name,
"{DiscNumber}", fmt.Sprintf("%0d", track.Attributes.DiscNumber),
"{TrackNumber}", fmt.Sprintf("%0d", track.Attributes.TrackNumber),
"{Quality}", Quality,
"{Tag}", Tag_string,
"{Codec}", Codec,
).Replace(config.SongFileFormat)
fmt.Println(songName)
filename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_"))
if dl_atmos {
filename = fmt.Sprintf("%s.ec3", forbiddenNames.ReplaceAllString(songName, "_"))
}
m4afilename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_"))
lrcFilename := fmt.Sprintf("%s.lrc", forbiddenNames.ReplaceAllString(songName, "_"))
trackPath := filepath.Join(sanAlbumFolder, filename)
m4atrackPath := filepath.Join(sanAlbumFolder, m4afilename)
var lrc string = ""
if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) {
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
if err != nil {
fmt.Printf("Failed to parse lyrics: %s \n", err)
fmt.Println("Failed to get lyrics")
} else {
if config.SaveLrcFile {
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
if err != nil {
fmt.Printf("Failed to write lyrics")
}
if !config.EmbedLrc {
lrc = ""
lrc, err = conventTTMLToLRC(ttml)
if err != nil {
fmt.Printf("Failed to parse lyrics: %s \n", err)
} else {
if config.SaveLrcFile {
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
if err != nil {
fmt.Printf("Failed to write lyrics")
}
if !config.EmbedLrc {
lrc = ""
}
}
}
}
}
}
exists, err := fileExists(trackPath)
if err != nil {
fmt.Println("Failed to check if track exists.")
}
if exists {
fmt.Println("Track already exists locally.")
oktrackNum += 1
continue
}
exists, err := fileExists(trackPath)
if err != nil {
fmt.Println("Failed to check if track exists.")
}
if exists {
fmt.Println("Track already exists locally.")
oktrackNum += 1
continue
}
m4aexists, err := fileExists(m4atrackPath)
if err != nil {
fmt.Println("Failed to check if track exists.")
}
if m4aexists {
fmt.Println("Track already exists locally.")
oktrackNum += 1
continue
}
trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil {
fmt.Println("Failed to extract info from manifest.\n", err)
continue
}
info, err := extractSong(trackUrl)
if err != nil {
fmt.Println("Failed to extract track.", err)
continue
}
samplesOk := true
for samplesOk {
for _, i := range info.samples {
if int(i.descIndex) >= len(keys) {
fmt.Println("Decryption size mismatch.")
samplesOk = false
trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil {
fmt.Println("Failed to extract info from manifest.\n", err)
continue
}
info, err := extractSong(trackUrl)
if err != nil {
fmt.Println("Failed to extract track.", err)
continue
}
samplesOk := true
for samplesOk {
for _, i := range info.samples {
if int(i.descIndex) >= len(keys) {
fmt.Println("Decryption size mismatch.")
samplesOk = false
}
}
break
}
break
}
if !samplesOk {
continue
}
err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal)
if err != nil {
fmt.Println("Failed to decrypt track.\n", err)
continue
}
tags := []string{
fmt.Sprintf("lyrics=%s", lrc),
}
if track.Attributes.ContentRating == "explicit" {
tags = append(tags, "rating=1")
} else if track.Attributes.ContentRating == "clean" {
tags = append(tags, "rating=2")
} else {
tags = append(tags, "rating=0")
}
if config.EmbedCover {
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL)
if err != nil {
fmt.Println("Failed to write cover.")
if !samplesOk {
continue
}
err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal)
if err != nil {
fmt.Println("Failed to decrypt track.\n", err)
continue
}
tags := []string{
fmt.Sprintf("lyrics=%s", lrc),
}
index := trackNum - 1
if dl_atmos {
tags = []string{
"tool=",
fmt.Sprintf("lyrics=%s", lrc),
fmt.Sprintf("title=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name),
fmt.Sprintf("artist=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName),
fmt.Sprintf("genre=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames[0]),
fmt.Sprintf("created=%s", meta.Data[0].Attributes.ReleaseDate),
fmt.Sprintf("album_artist=%s", meta.Data[0].Attributes.ArtistName),
fmt.Sprintf("composer=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName),
fmt.Sprintf("writer=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName),
fmt.Sprintf("performer=%s", meta.Data[0].Attributes.ArtistName),
fmt.Sprintf("copyright=%s", meta.Data[0].Attributes.Copyright),
fmt.Sprintf("ISRC=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Isrc),
fmt.Sprintf("UPC=%s", meta.Data[0].Attributes.Upc),
}
if strings.Contains(albumId, "pl.") && !config.UseSongInfoForPlaylist {
tags = append(tags, "disk=1/1")
tags = append(tags, fmt.Sprintf("track=%s", trackNum))
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", trackNum, trackTotal))
tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Attributes.Name))
} else {
tags = append(tags, fmt.Sprintf("disk=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber, meta.Data[0].Relationships.Tracks.Data[trackTotal-1].Attributes.DiscNumber))
tags = append(tags, fmt.Sprintf("track=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber))
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber, trackTotal))
tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName))
}
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat))
}
if track.Attributes.ContentRating == "explicit" {
tags = append(tags, "rating=1")
} else if track.Attributes.ContentRating == "clean" {
tags = append(tags, "rating=2")
} else {
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", config.CoverFormat))
tags = append(tags, "rating=0")
}
}
tagsString := strings.Join(tags, ":")
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath)
if err := cmd.Run(); err != nil {
fmt.Printf("Embed failed: %v\n", err)
continue
}
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)); err != nil {
fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder, track.ID, config.CoverFormat)
if config.EmbedCover {
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL)
if err != nil {
fmt.Println("Failed to write cover.")
}
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat))
} else {
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", config.CoverFormat))
}
}
tagsString := strings.Join(tags, ":")
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath)
if dl_atmos {
cmd = exec.Command("MP4Box", "-add", trackPath, "-name", fmt.Sprintf("1=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name), "-itags", tagsString, "-brand", "mp42", "-ab", "dby1", m4atrackPath)
}
if err := cmd.Run(); err != nil {
fmt.Printf("Embed failed: %v\n", err)
continue
}
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)); err != nil {
fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder, track.ID, config.CoverFormat)
continue
}
}
if dl_atmos {
fmt.Printf("Deleting original EC3 file: %s\n", filepath.Base(trackPath))
if err := os.Remove(trackPath); err != nil {
fmt.Printf("Error deleting file: %v\n", err)
continue
}
fmt.Printf("Successfully processed and deleted %s\n", filepath.Base(trackPath))
}
oktrackNum += 1
}
oktrackNum += 1
}
return err
}
@ -1614,6 +1747,17 @@ func main() {
fmt.Println("Failed to get token.")
return
}
var dlArgs []string
for _, arg := range os.Args {
if strings.Contains(arg, "--atmos") {
dl_atmos = true
} else if strings.Contains(arg, "--select") {
dl_select = true
} else {
dlArgs = append(dlArgs, arg)
}
}
os.Args = dlArgs
if strings.Contains(os.Args[1], "/artist/") {
newArgs, err := checkArtist(os.Args[1], token)
if err != nil {
@ -1882,26 +2026,46 @@ func extractMedia(b string) (string, []string, error) {
return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth
})
for _, variant := range master.Variants {
if variant.Codecs == "alac" {
split := strings.Split(variant.Audio, "-")
length := len(split)
length_int, err := strconv.Atoi(split[length-2])
if err != nil {
return "", nil, err
if dl_atmos {
if variant.Codecs == "ec-3" {
split := strings.Split(variant.Audio, "-")
length := len(split)
length_int, err := strconv.Atoi(split[length-1])
if err != nil {
return "", nil, err
}
if length_int <= config.AtmosMax {
fmt.Printf("%s\n", variant.Audio)
streamUrlTemp, err := masterUrl.Parse(variant.URI)
if err != nil {
panic(err)
}
streamUrl = streamUrlTemp
break
}
}
if length_int <= config.AlacMax {
fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2])
streamUrlTemp, err := masterUrl.Parse(variant.URI)
} else {
if variant.Codecs == "alac" {
split := strings.Split(variant.Audio, "-")
length := len(split)
length_int, err := strconv.Atoi(split[length-2])
if err != nil {
panic(err)
return "", nil, err
}
if length_int <= config.AlacMax {
fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2])
streamUrlTemp, err := masterUrl.Parse(variant.URI)
if err != nil {
panic(err)
}
streamUrl = streamUrlTemp
break
}
streamUrl = streamUrlTemp
break
}
}
}
if streamUrl == nil {
return "", nil, errors.New("no alac codec found")
return "", nil, errors.New("no codec found")
}
var keys []string
keys = append(keys, prefetchKey)
@ -1909,8 +2073,14 @@ func extractMedia(b string) (string, []string, error) {
regex := regexp.MustCompile(`"(skd?://[^"]*)"`)
matches := regex.FindAllStringSubmatch(masterString, -1)
for _, match := range matches {
if strings.HasSuffix(match[1], "c23") || strings.HasSuffix(match[1], "c6") {
keys = append(keys, match[1])
if dl_atmos {
if strings.HasSuffix(match[1], "c24") || strings.HasSuffix(match[1], "c6") {
keys = append(keys, match[1])
}
} else {
if strings.HasSuffix(match[1], "c23") || strings.HasSuffix(match[1], "c6") {
keys = append(keys, match[1])
}
}
}
return streamUrl.String(), keys, nil
@ -1992,23 +2162,30 @@ func extractSong(url string) (*SongInfo, error) {
return nil, err
}
enca, err := mp4.ExtractBoxWithPayload(f, stbl[0], []mp4.BoxType{
mp4.BoxTypeStsd(),
mp4.BoxTypeEnca(),
})
if err != nil {
return nil, err
}
aalac, err := mp4.ExtractBoxWithPayload(f, &enca[0].Info,
[]mp4.BoxType{BoxTypeAlac()})
if err != nil || len(aalac) != 1 {
return nil, err
}
var extracted *SongInfo
if !dl_atmos {
enca, err := mp4.ExtractBoxWithPayload(f, stbl[0], []mp4.BoxType{
mp4.BoxTypeStsd(),
mp4.BoxTypeEnca(),
})
if err != nil {
return nil, err
}
extracted := &SongInfo{
r: f,
alacParam: aalac[0].Payload.(*Alac),
aalac, err := mp4.ExtractBoxWithPayload(f, &enca[0].Info,
[]mp4.BoxType{BoxTypeAlac()})
if err != nil || len(aalac) != 1 {
return nil, err
}
extracted = &SongInfo{
r: f,
alacParam: aalac[0].Payload.(*Alac),
}
} else {
extracted = &SongInfo{
r: f,
// alacParam: aalac[0].Payload.(*Alac),
}
}
moofs, err := mp4.ExtractBox(f, nil, []mp4.BoxType{

Loading…
Cancel
Save