Skip to content

Commit

Permalink
Fix #58
Browse files Browse the repository at this point in the history
All available episode of the show are downloaded. The full list is now took
"toutes les videos" page.
  • Loading branch information
simulot committed Feb 4, 2021
1 parent 1e8c519 commit 86219fc
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 121 deletions.
27 changes: 14 additions & 13 deletions cmd/aspiratv/main.go
Expand Up @@ -244,19 +244,18 @@ func (a *app) Download(ctx context.Context) {
}

for dl := 1; dl < flag.NArg(); dl++ {
mr := matcher.MatchRequest{
Destination: "DL",
Show: strings.ToLower(flag.Arg(dl)),
Provider: a.Config.Provider,
MaxAgedDays: a.Config.MaxAgedDays,
RetentionDays: a.Config.RetentionDays,
TitleFilter: filter,
TitleExclude: exclude,
ShowRootPath: a.Config.ShowPath,
}
a.Config.WatchList = append(a.Config.WatchList, &mr)

a.Config.WatchList = append(a.Config.WatchList,
&matcher.MatchRequest{
Destination: "DL",
Show: strings.ToLower(flag.Arg(dl)),
Provider: a.Config.Provider,
MaxAgedDays: a.Config.MaxAgedDays,
RetentionDays: a.Config.RetentionDays,
TitleFilter: filter,
TitleExclude: exclude,
ShowRootPath: a.Config.ShowPath,
},
)
}
a.worker = workers.New(ctx, a.Config.ConcurrentTasks, a.logger) //TODO
a.getter = myhttp.DefaultClient
Expand Down Expand Up @@ -385,14 +384,16 @@ func (a *app) PullShows(ctx context.Context, p providers.Provider, pc *mpb.Progr
providerBar.SetPriority(int(atomic.AddInt32(&nbPuller, 1)))
}

a.logger.Trace().Printf("Get shows list for %s", p.Name())
a.logger.Trace().Printf("[%s] Get shows list", p.Name())
seen := map[string]bool{}
wg := sync.WaitGroup{}

showCount := int64(0)
showLoop:

for m := range p.MediaList(ctx, a.Config.WatchList) {
a.logger.Trace().Printf("[%s] Get id %s", p.Name(), m.ID)

if _, ok := seen[m.ID]; ok {
continue
}
Expand Down
166 changes: 166 additions & 0 deletions providers/francetv/details.go
@@ -0,0 +1,166 @@
package francetv

import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"

"github.com/gocolly/colly"
"github.com/simulot/aspiratv/metadata/nfo"
"github.com/simulot/aspiratv/net/myhttp/httptest"
"github.com/simulot/aspiratv/providers"
)

type FTVPlayerVideo struct {
ContentID int `json:"contentId"`
VideoID string `json:"videoId"`
// EndDate time.Time `json:"endDate"`
// OriginURL string `json:"originUrl"`
// // ComingNext ComingNext `json:"comingNext"`
// IsSponsored bool `json:"isSponsored"`
// IsAdVisible interface{} `json:"isAdVisible"`
// VideoTitle string `json:"videoTitle"`
// ProgramName string `json:"programName"`
// SeasonNumber int `json:"seasonNumber"`
// EpisodeNumber int `json:"episodeNumber"`
// LayerType interface{} `json:"layerType"`
// RatingCsaCode string `json:"ratingCsaCode"`
// Logo interface{} `json:"logo"`
// Name interface{} `json:"name"`
// // BroadcastBeginDate BroadcastBeginDate `json:"broadcastBeginDate"`
// Intro bool `json:"intro"`
}

// GetMediaDetails download more details when available especially the stream URL.
// The player webservice returns some metadata and an URL named Token.
// The must been acquired right before the actual download. It has a limited validity
// In the structure returned by token URL, another URL is provided. The request is then redirected
// to the actual video stream. This url has also a limited validity.
//
// But for some reason FFMPEG doesn't follow the redirection. So, we have to get the final URL before
// calling FFMPEG // FranceTV provides a subtitle tracks that isn't decoded by FFMPEG.
// And FFMPEG doesn't get always the best video resolution
//
// The video stream is in fact a MPD manifest files. We can edit this manifest for removing unwanted tracks.
//

func (p *FranceTV) GetMediaDetails(ctx context.Context, m *providers.Media) error {
info := m.Metadata.GetMediaInfo()
parser := p.htmlParserFactory.New() // TODO withContext
videoID := ""

parser.OnHTML("meta", func(e *colly.HTMLElement) {
switch e.Attr("property") {
case "og:image":
info.Thumb = append(info.Thumb, nfo.Thumb{
URL: e.Attr("content"),
})
case "og:description":
info.Plot = e.Attr("content")
case "video:actor":
for _, a := range strings.Split(e.Attr("content"), ",") {
info.Actor = append(info.Actor, nfo.Actor{Name: strings.TrimSpace(a)})
}
case "video:director":
for _, a := range strings.Split(e.Attr("content"), ",") {
info.Director = append(info.Director, a)
}
case "video:release_date":
t, _ := time.Parse("2006-01-02T15:04:05-07:00", e.Attr("content"))
info.Aired = nfo.Aired(t)
}

})

parser.OnHTML("div.l-content script", func(e *colly.HTMLElement) {
if !strings.Contains(e.Text, "FTVPlayerVideos") {
return
}
start := strings.Index(e.Text, "[")
end := strings.Index(e.Text, "];")
if start < 0 || end < 0 {
return
}

s := e.Text[:end+1][start:]
// videos := []FTVPlayerVideos{}
var videos []FTVPlayerVideo

err := json.Unmarshal([]byte(s), &videos)
if err != nil {
p.config.Log.Error().Printf("[%s] Can't decode FTVPlayerVideo: %s", p.Name(), err)
return
}
videoID = videos[0].VideoID
})
err := parser.Visit(info.PageURL)
if err != nil {
return err
}

err = p.getMediaURL(ctx, info, videoID)
return err
}

func (p *FranceTV) getMediaURL(ctx context.Context, info *nfo.MediaInfo, videoID string) error {
v := url.Values{}
v.Set("country_code", "FR")
v.Set("w", "1920")
v.Set("h", "1080")
v.Set("version", "5.18.3")
v.Set("domain", "www.france.tv")
v.Set("device_type", "desktop")
v.Set("browser", "firefox")
v.Set("browser_version", "85")
v.Set("os", "windows")
v.Set("gmt", "+1")

u := "https://player.webservices.francetelevisions.fr/v1/videos/" + videoID + "?" + v.Encode()
p.config.Log.Debug().Printf("[%s] Player URL for title '%s' is %q.", p.Name(), info.Title, u)

r, err := p.getter.Get(ctx, u)
if err != nil {
return fmt.Errorf("Can't get player: %w", err)
}
if p.config.Log.IsDebug() {
r = httptest.DumpReaderToFile(p.config.Log, r, "francetv-player-"+videoID+"-")
}
defer r.Close()

pl := player{}
err = json.NewDecoder(r).Decode(&pl)
if err != nil {
return fmt.Errorf("Can't decode player: %w", err)
}

// Get Token
if len(pl.Video.Token) > 0 {
p.config.Log.Debug().Printf("[%s] Player token for '%s' is %q ", p.Name(), info.Title, pl.Video.Token)

r2, err := p.getter.Get(ctx, pl.Video.Token)
if err != nil {
return fmt.Errorf("Can't get token %s: %w", pl.Video.Token, err)
}
if p.config.Log.IsDebug() {
r2 = httptest.DumpReaderToFile(p.config.Log, r2, "francetv-token-"+videoID+"-")
}
defer r2.Close()
pl := struct {
URL string `json:"url"`
}{}
err = json.NewDecoder(r2).Decode(&pl)
if err != nil {
return fmt.Errorf("Can't decode token's url : %w", err)
}
if len(pl.URL) == 0 {
return fmt.Errorf("Show's URL is empty")
}
info.MediaURL = pl.URL

}
p.config.Log.Trace().Printf("[%s] Player URL for '%s' is %q ", p.Name(), info.Title, info.MediaURL)
return nil
}
106 changes: 13 additions & 93 deletions providers/francetv/francetv.go
Expand Up @@ -2,17 +2,12 @@ package francetv

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strconv"
"sync"
"time"

"github.com/simulot/aspiratv/net/myhttp/httptest"
"github.com/simulot/aspiratv/parsers/htmlparser"
"github.com/simulot/aspiratv/providers/matcher"

"github.com/simulot/aspiratv/net/myhttp"
Expand Down Expand Up @@ -40,11 +35,12 @@ type getter interface {

// FranceTV structure handles france-tv catalog of shows
type FranceTV struct {
getter getter
deadline time.Duration
seasons sync.Map
shows sync.Map
config providers.Config
getter getter
htmlParserFactory *htmlparser.Factory
deadline time.Duration
seasons sync.Map
shows sync.Map
config providers.Config
}

// WithGetter inject a getter in FranceTV object instead of normal one
Expand All @@ -57,8 +53,9 @@ func WithGetter(g getter) func(ftv *FranceTV) {
// New setup a Show provider for France Télévisions
func New() (*FranceTV, error) {
p := &FranceTV{
getter: myhttp.DefaultClient,
deadline: 30 * time.Second,
getter: myhttp.DefaultClient,
deadline: 30 * time.Second,
htmlParserFactory: htmlparser.NewFactory(),
}
return p, nil
}
Expand All @@ -80,13 +77,16 @@ func (p *FranceTV) MediaList(ctx context.Context, mm []*matcher.MatchRequest) ch
go func() {
defer close(shows)
for _, m := range mm {
p.config.Log.Trace().Printf("[%s] Check matching request for %q", p.Name(), m.Show)

if m.Provider != "francetv" {
continue
}
for s := range p.search(ctx, m) {
shows <- s
}
}
p.config.Log.Trace().Printf("[%s] MediaList is done", p.Name())
}()
return shows
}
Expand All @@ -106,83 +106,3 @@ type player struct {
ImageURL string `json:"image_url"`
} `json:"meta"`
}

// GetMediaDetails download more details when available especially the stream URL.
// The player webservice returns some metadata and an URL named Token.
// The must been acquired right before the actual download. It has a limited validity
// In the structure returned by token URL, another URL is provided. The request is then redirected
// to the actual video stream. This url has also a limited validity.
//
// But for some reason FFMPEG doesn't follow the redirection. So, we have to get the final URL before
// calling FFMPEG // FranceTV provides a subtitle tracks that isn't decoded by FFMPEG.
// And FFMPEG doesn't get always the best video resolution
//
// The video stream is in fact a MPD manifest files. We can edit this manifest for removing unwanted tracks.
//
func (p *FranceTV) GetMediaDetails(ctx context.Context, m *providers.Media) error {
info := m.Metadata.GetMediaInfo()
v := url.Values{}
v.Set("country_code", "FR")
v.Set("w", "1920")
v.Set("h", "1080")
v.Set("version", "5.18.3")
v.Set("domain", "www.france.tv")
v.Set("device_type", "desktop")
v.Set("browser", "firefox")
v.Set("browser_version", "75")
v.Set("os", "windows")
v.Set("gmt", "+1")

u := "https://player.webservices.francetelevisions.fr/v1/videos/" + m.ID + "?" + v.Encode()
p.config.Log.Debug().Printf("[%s] Player URL for title '%s' is %q.", p.Name(), m.Metadata.GetMediaInfo().Title, u)

r, err := p.getter.Get(ctx, u)
if err != nil {
return fmt.Errorf("Can't get player: %w", err)
}
if p.config.Log.IsDebug() {
r = httptest.DumpReaderToFile(p.config.Log, r, "francetv-player-"+m.ID+"-")
}
defer r.Close()

pl := player{}
err = json.NewDecoder(r).Decode(&pl)
if err != nil {
return fmt.Errorf("Can't decode player: %w", err)
}

episodeRegexp := regexp.MustCompile(`S(\d+)\sE(\d+)`)
expr := episodeRegexp.FindAllStringSubmatch(pl.Meta.PreTitle, -1)
if len(expr) > 0 {
info.Season, _ = strconv.Atoi(expr[0][1])
info.Episode, _ = strconv.Atoi(expr[0][2])
}

// Get Token
if len(pl.Video.Token) > 0 {
p.config.Log.Debug().Printf("[%s] Player token for '%s' is %q ", p.Name(), m.Metadata.GetMediaInfo().Title, pl.Video.Token)

r2, err := p.getter.Get(ctx, pl.Video.Token)
if err != nil {
return fmt.Errorf("Can't get token %s: %w", pl.Video.Token, err)
}
if p.config.Log.IsDebug() {
r2 = httptest.DumpReaderToFile(p.config.Log, r2, "francetv-token-"+m.ID+"-")
}
defer r2.Close()
pl := struct {
URL string `json:"url"`
}{}
err = json.NewDecoder(r2).Decode(&pl)
if err != nil {
return fmt.Errorf("Can't decode token's url : %w", err)
}
if len(pl.URL) == 0 {
return fmt.Errorf("Show's URL is empty")
}
info.MediaURL = pl.URL

}
p.config.Log.Trace().Printf("[%s] Player URL for '%s' is %q ", p.Name(), m.Metadata.GetMediaInfo().Title, info.MediaURL)
return nil
}

0 comments on commit 86219fc

Please sign in to comment.