diff --git a/client.go b/client.go index 2bb7c55..307251f 100644 --- a/client.go +++ b/client.go @@ -7,9 +7,12 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "os" + "path/filepath" "regexp" "strings" + "text/tabwriter" "time" "github.com/anacrolix/torrent" @@ -21,7 +24,10 @@ const clearScreen = "\033[H\033[2J" const torrentBlockListURL = "http://john.bitsurge.net/public/biglist.p2p.gz" -var isHTTP = regexp.MustCompile(`^https?:\/\/`) +var ( + isHTTP = regexp.MustCompile(`^https?:\/\/`) + isVideo = regexp.MustCompile(`\.(mkv|ogm|webm|flv|avi|mov|wmv|mp4|mpe?g|vob)$`) +) // ClientError formats errors coming from the client. type ClientError struct { @@ -111,14 +117,6 @@ func NewClient(cfg ClientConfig) (client Client, err error) { client.Torrent = t client.Torrent.SetMaxEstablishedConns(cfg.MaxConnections) - go func() { - <-t.GotInfo() - t.DownloadAll() - - // Prioritize first 5% of the file. - client.getLargestFile().PrioritizeRegion(0, int64(t.NumPieces()/100*5)) - }() - go client.addBlocklist() return @@ -261,10 +259,27 @@ func (c Client) ReadyForPlayback() bool { return c.percentage() > 5 } -// GetFile is an http handler to serve the biggest file managed by the client. +// GetFile is an http handler to serve the file specified in the URL. func (c Client) GetFile(w http.ResponseWriter, r *http.Request) { - target := c.getLargestFile() - entry, err := NewFileReader(target) + // clean up request path '/foo+bar.mkv' -> 'foo bar.mkv' + path, err := url.QueryUnescape(strings.TrimLeft(r.RequestURI, "/")) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // find it by display path + var target torrent.File + for _, file := range c.Torrent.Files() { + if file.DisplayPath() == path { + target = file + } + } + if target.Path() == "" { + http.NotFound(w, r) + return + } + + entry, err := NewFileReader(&target) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -276,8 +291,45 @@ func (c Client) GetFile(w http.ResponseWriter, r *http.Request) { } }() - w.Header().Set("Content-Disposition", "attachment; filename=\""+c.Torrent.Info().Name+"\"") - http.ServeContent(w, r, target.DisplayPath(), time.Now(), entry) + filename := filepath.Base(target.Path()) + w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"") + http.ServeContent(w, r, filename, time.Now(), entry) +} + +// SelectFile asks the user to select a video if needed. +// If there's a single video, it chosen automatically. +// If there's no video, the user may select any file. +// If there are multiple videos, the user may select any of those. +func (c Client) SelectFile() *torrent.File { + candidates := []torrent.File{} + for _, file := range c.Torrent.Files() { + if isVideo.MatchString(file.Path()) { + candidates = append(candidates, file) + } + } + if len(candidates) == 0 { + candidates = c.Torrent.Files() + } + if len(candidates) == 1 { + return &candidates[0] + } + fmt.Println() + w := tabwriter.NewWriter(os.Stdout, 1, 4, 1, ' ', 0) + fmt.Fprintln(w, "#\tPath\tSize") + for i, file := range candidates { + fmt.Fprintf(w, "%d\t%s\t%s\n", i, file.DisplayPath(), humanize.Bytes(uint64(file.Length()))) + } + err := w.Flush() + if err != nil { + log.Println(err) + } + pos := -1 + for pos < 0 || pos >= len(candidates) { + fmt.Println() + fmt.Printf("Enter number of file to open [0-%d]\n", len(candidates)-1) + fmt.Scanln(&pos) + } + return &candidates[pos] } func (c Client) percentage() float64 { diff --git a/main.go b/main.go index 3056e4c..b8d1198 100644 --- a/main.go +++ b/main.go @@ -47,16 +47,6 @@ func main() { log.Fatal(http.ListenAndServe(":"+strconv.Itoa(cfg.Port), nil)) }() - // Open selected video player - if *player != "" { - go func() { - for !client.ReadyForPlayback() { - time.Sleep(time.Second) - } - openPlayer(*player, cfg.Port) - }() - } - // Handle exit signals. interruptChannel := make(chan os.Signal, 1) signal.Notify(interruptChannel, @@ -73,6 +63,14 @@ func main() { } }(interruptChannel) + <-client.Torrent.GotInfo() + log.Printf("Now streaming: %s", client.Torrent.Name()) + + // Open selected video player + if *player != "" { + openPlayer(*player, cfg.Port, client.SelectFile().DisplayPath()) + } + // Cli render loop. for { client.Render() diff --git a/player.go b/player.go index a1a3974..0f78037 100644 --- a/player.go +++ b/player.go @@ -1,10 +1,11 @@ package main import ( + "fmt" "log" + "net/url" "os/exec" "runtime" - "strconv" "strings" ) @@ -36,8 +37,8 @@ func (p GenericPlayer) Open(url string) error { return exec.Command(command[0], command[1:]...).Start() } -// openPlayer opens a stream using the specified player and port. -func openPlayer(playerName string, port int) { +// openPlayer opens a stream using the specified player, port and file path. +func openPlayer(playerName string, port int, file string) { var player Player playerName = strings.ToLower(playerName) for _, genericPlayer := range genericPlayers { @@ -49,8 +50,9 @@ func openPlayer(playerName string, port int) { log.Printf("Player '%s' is not supported. Currently supported players are: %s", playerName, joinPlayerNames()) return } - log.Printf("Playing in %s", playerName) - if err := player.Open("http://localhost:" + strconv.Itoa(port)); err != nil { + uri := fmt.Sprintf("http://localhost:%d/%s", port, url.QueryEscape(file)) + log.Printf("Playing in %s: %s", playerName, uri) + if err := player.Open(uri); err != nil { log.Printf("Error opening %s: %s\n", playerName, err) } }