<|start_filename|>cmd/osdb/cmd/imdb.go<|end_filename|>
package cmd

import (
	"fmt"
	"os"
	"strings"
	"text/tabwriter"

	"github.com/oz/osdb"
	"github.com/spf13/cobra"
)

var client *osdb.Client

func init() {
	imdbCmd.AddCommand(imdbShowCmd)
	RootCmd.AddCommand(imdbCmd)
}

var imdbCmd = &cobra.Command{
	Use:   "imdb [query]",
	Short: "Search IMDB",
	Long:  `Search IMDB for a movie, through OSDB's API.`,
	PersistentPreRun: func(cmd *cobra.Command, args []string) {
		c, err := InitClient(os.Getenv("OSDB_LANG"))
		if err != nil {
			fmt.Printf("Error: %s\n", err)
			return
		}
		client = c
	},
	Run: func(cmd *cobra.Command, args []string) {
		q := strings.Join(args, " ")
		fmt.Printf("Searching %s on IMDB...\n\n", q)
		movies, err := client.IMDBSearch(q)
		if err != nil {
			fmt.Printf("Error: %s\n", err)
			return
		}
		if movies.Empty() {
			fmt.Println("No results.")
		}
		for _, m := range movies {
			fmt.Printf("%s %s http://www.imdb.com/title/tt%s/\n", m.ID, m.Title, m.ID)
		}
	},
}

var imdbShowCmd = &cobra.Command{
	Use:   "show [imdb_ids...]",
	Short: "Show movie details",
	Long:  `Display movie facts for an IMDB movie.`,
	PreRun: func(cmd *cobra.Command, args []string) {
		if len(args) < 1 {
			fmt.Println("Missing movie ID")
			os.Exit(1)
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		for _, id := range args {
			m, err := client.GetIMDBMovieDetails(id)
			if err != nil {
				fmt.Printf("Error: %s\n", err)
				return
			}
			showMovieDetails(m)
		}
	},
}

func showMovieDetails(m *osdb.Movie) {
	w := new(tabwriter.Writer)
	w.Init(os.Stdout, 0, 8, 0, '\t', 0)
	fmt.Fprintf(w, "IMDB Id:\t%s\n", m.ID)
	fmt.Fprintf(w, "Title:\t%s\n", m.Title)
	fmt.Fprintf(w, "Year:\t%s\n", m.Year)
	fmt.Fprintf(w, "Duration:\t%s\n", m.Duration)
	fmt.Fprintf(w, "Cover:\t%s\n", m.Cover)
	fmt.Fprintf(w, "TagLine:\t%s\n", m.TagLine)
	fmt.Fprintf(w, "Plot:\t%s\n", m.Plot)
	fmt.Fprintf(w, "Goofs:\t%s\n", m.Goofs)
	fmt.Fprintf(w, "Trivia:\t%s\n", m.Trivia)
	w.Flush()
	fmt.Println()
}


<|start_filename|>subtitle_test.go<|end_filename|>
package osdb

import (
	"io/ioutil"
	"os"
	"testing"
)

func TestBestWithNoResults(t *testing.T) {
	subs := Subtitles{}
	res := subs.Best()
	if res != nil {
		t.Fatalf("Expected nil, got: %v", res)
	}
}

func TestBestWithResults(t *testing.T) {
	subs := Subtitles{
		Subtitle{MovieName: "Go", SubDownloadsCnt: "1"},
		Subtitle{MovieName: "Never Let Me Go", SubDownloadsCnt: "2"},
		Subtitle{MovieName: "Don't Let Me Go", SubDownloadsCnt: "3"},
	}
	res := subs.Best()
	if res == nil {
		t.Fatalf("Expected Subtitle, got: %v", res)
	}
	if res.MovieName != "Don't Let Me Go" {
		t.Fatalf("Expected Don't Let Me Go, got: %v", res)
	}
}

func TestNewSubtitle(t *testing.T) {
	data := make([]byte, ChunkSize*2)

	// Generate dummy movie file
	copy(data, []byte("raw movie bytes go here"))
	movieHash := "8a4f474701fbf13e"
	err := ioutil.WriteFile("./test-movie.avi", data, 0644)
	if err != nil {
		t.Fatalf("Can't create test-movie.avi")
	}
	defer os.Remove("./test-movie.avi")

	// Generate dummy subtitles file
	copy(data, []byte("subtitle file data goes here"))
	subHash := "95ad809f6ee45b779abf6d5337efeb44"
	err = ioutil.WriteFile("./test-movie.srt", data, 0644)
	if err != nil {
		t.Fatalf("Can't create test-movie.srt")
	}
	defer os.Remove("./test-movie.srt")

	s, err := NewSubtitle("./test-movie.avi", "./test-movie.srt", "TEST")
	if err != nil {
		t.Fatalf("Expected Subtitle, got error: %v", err)
	}

	if s.SubLanguageID != "TEST" {
		t.Fatalf("Expected SubLanguageID \"TEST\", got %s", s.SubLanguageID)
	}

	if s.SubFileName != "test-movie.srt" {
		t.Fatalf("Expected SubFileName test-movie.srt, got %s", s.SubFileName)
	}
	if s.MovieFileName != "test-movie.avi" {
		t.Fatalf("Expected MovieFileName test-movie.avi, got %s", s.MovieFileName)
	}
	if s.SubHash != subHash {
		t.Fatalf("Expected subtitle Hash %s, got %s", subHash, s.SubHash)
	}
	if s.MovieHash != movieHash {
		t.Fatalf("Expected movie Hash %s, got %s", movieHash, s.MovieHash)
	}
}


<|start_filename|>osdb_test.go<|end_filename|>
package osdb

import (
	"io/ioutil"
	"os"
	"testing"
)

func TestHashWithNonExistentFile(t *testing.T) {
	_, err := Hash("./nonexistent")
	if err == nil {
		t.Fatalf("Expected an error, got none")

	}
}

func TestHashWithTooSmallFile(t *testing.T) {
	err := ioutil.WriteFile("./small", []byte("too small"), 0644)
	defer os.Remove("./small")
	if err != nil {
		t.Fatalf("Can't create small file")
	}

	_, err = Hash("./small")
	if err == nil {
		t.Fatalf("Expected an error, got none")
	}
}

func TestHashWithSample(t *testing.T) {
	data := make([]byte, ChunkSize*2)
	var dataHash uint64 = 0x6c62616c62636cc3
	copy(data, []byte("blablabla"))

	err := ioutil.WriteFile("./sample", data, 0644)
	if err != nil {
		t.Fatalf("Can't create sample file")
	}
	defer os.Remove("./sample")

	hash, err := Hash("./sample")
	if err != nil {
		t.Fatalf("Expected hash, got error: %v", err)
	}
	if hash != dataHash {
		t.Fatalf("Expected hash 0x%016x, got 0x%016x", dataHash, hash)
	}
}

func TestNewClient(t *testing.T) {
	_, err := NewClient()
	if err != nil {
		t.Fatalf("Can't allocate new client: %v", err)
	}
}


<|start_filename|>client_test.go<|end_filename|>
// +build live

package osdb

import (
	"bufio"
	"fmt"
	"testing"
	"time"
)

func ExampleClient_BestMoviesByHashes() {
	c, err := NewClient()
	if err != nil {
		fmt.Printf("can't create client: %s\n", err)
		return
	}

	err = c.LogIn("", "", "")
	if err != nil {
		fmt.Printf("can't login: %s\n", err)
		return
	}

	hashes := []uint64{0x09a2c497663259cb, 0x46e33be00464c12e}
	movies, err := c.BestMoviesByHashes(hashes)
	if err != nil {
		fmt.Printf("can't search: %s\n", err)
		return
	}

	for i, hash := range hashes {
		if movies[i] != nil {
			fmt.Printf("%016x: %s (%s) - id %s\n", hash,
				movies[i].Title, movies[i].Year, movies[i].ID)
		} else {
			fmt.Printf("%016x: unknown\n", hash)
		}
	}

	// Output:
	// 09a2c497663259cb: Nochnoy dozor (2004) - id 0403358
	// 46e33be00464c12e: "Game of Thrones" Two Swords (2014) - id 2816136
}

func ExampleClient_IMDBSearchByID() {
	c, err := NewClient()
	if err != nil {
		fmt.Printf("can't create client: %s\n", err)
		return
	}

	err = c.LogIn("", "", "")
	if err != nil {
		fmt.Printf("can't login: %s\n", err)
		return
	}

	ids := []string{"0403358"}
	langs := []string{"eng", "rus"}

	time.Sleep(1 * time.Second)
	subs, err := c.IMDBSearchByID(ids, langs)
	if err != nil {
		fmt.Printf("can't search: %s\n", err)
		return
	}

	for _, sub := range subs {
		if sub.SubHash == "fb1a2837e1e6a4cceeb237154fac5f21" {

			// check if that top rated subtitle have been returned
			fmt.Printf("%s: %s\n", sub.IDMovieImdb, sub.SubFileName)
		}
	}

	// please note that sometimes Opensubtitles will remove leading zeroes!

	// Output:
	// 403358: Night.Watch.2004.720p.BluRay.x264-SiNNERS.srt
}

func ExampleClient_GetIMDBMovieDetails() {
	c, err := NewClient()
	if err != nil {
		fmt.Printf("can't create client: %s\n", err)
		return
	}

	err = c.LogIn("", "", "")
	if err != nil {
		fmt.Printf("can't login: %s\n", err)
		return
	}

	id := "0403358"

	time.Sleep(1 * time.Second)
	movie, err := c.GetIMDBMovieDetails(id)
	if err != nil {
		fmt.Printf("can't get details: %s\n", err)
		return
	}
	fmt.Printf("%s: %s - id %s - awards '%s'\n", id, movie.Title, movie.ID, movie.Awards[0])

	// Output:
	// 0403358: Nochnoy dozor - id 0403358 - awards '2 wins & 5 nominations.'
}

func TestIMDBSearchByIDManyTimes(t *testing.T) {
	c, err := NewClient()
	if err != nil {
		fmt.Printf("can't create client: %s\n", err)
		return
	}

	err = c.LogIn("", "", "")
	if err != nil {
		fmt.Printf("can't login: %s\n", err)
		return
	}

	ids := []string{"0403358", "2816136"}
	langs := []string{"eng", "rus"}

	for i := 0; i < 3; i++ {
		_, err := c.IMDBSearchByID(ids, langs)
		if err != nil {
			t.Fail()
		}
	}
}

func ExampleClient_DownloadSubtitlesByIds() {
	c, err := NewClient()
	if err != nil {
		fmt.Printf("can't create client: %s\n", err)
		return
	}

	err = c.LogIn("", "", "")
	if err != nil {
		fmt.Printf("can't login: %s\n", err)
		return
	}

	ids := []int{1951968569, 1954123031}

	subFiles, err := c.DownloadSubtitlesByIds(ids)
	if err != nil {
		fmt.Printf("can't download subtitles: %s\n", err)
		return
	}

	for i, sf := range subFiles {
		reader, err := sf.Reader()
		if err != nil {
			fmt.Printf("can't open reader: %s\n", err)
			return
		}
		scanner := bufio.NewScanner(reader)
		for j := 0; j < 98; j++ {
			if !scanner.Scan() {
				fmt.Printf("too few lines in subtitle file\n")
				return
			}
		}
		if scanner.Scan() {
			fmt.Printf("99th line of subtitle %d: %s\n", i, scanner.Text())
		}
	}

	// Output:
	// 99th line of subtitle 0: ...and he knew that unless
	// 99th line of subtitle 1: and top it in many ways.
}

func ExampleClient_DownloadSubtitles() {
	c, err := NewClient()
	if err != nil {
		fmt.Printf("can't create client: %s\n", err)
		return
	}

	err = c.LogIn("", "", "")
	if err != nil {
		fmt.Printf("can't login: %s\n", err)
		return
	}

	ids := []string{"0403358", "2816136"}
	langs := []string{"eng", "rus"}

	subs, err := c.IMDBSearchByID(ids, langs)
	if err != nil {
		fmt.Printf("can't search: %s\n", err)
		return
	}

	// we now have a slice of subtitles for those two films in english and russian.

	var testSubtitles Subtitles

	for _, sub := range subs {
		if sub.SubHash == "fb1a2837e1e6a4cceeb237154fac5f21" ||
			sub.SubHash == "51988e72deb96e1d1abd79ac8daf4b3b" {
			testSubtitles = append(testSubtitles, sub)
		}
	}

	subFiles, err := c.DownloadSubtitles(testSubtitles)
	if err != nil {
		fmt.Printf("can't download subtitles: %s\n", err)
		return
	}

	for i, sf := range subFiles {
		reader, err := sf.Reader()
		if err != nil {
			fmt.Printf("can't open reader: %s\n", err)
			return
		}
		scanner := bufio.NewScanner(reader)
		for j := 0; j < 98; j++ {
			if !scanner.Scan() {
				fmt.Printf("too few lines in subtitle file\n")
				return
			}
		}
		if scanner.Scan() {
			fmt.Printf("99th line of subtitle %d: %s\n", i, scanner.Text())
		}
	}

	// Output:
	// 99th line of subtitle 0: ...and he knew that unless
	// 99th line of subtitle 1: and top it in many ways.
}

func ExampleClient_DownloadSubtitles_foreign() {
	c, err := NewClient()
	if err != nil {
		fmt.Printf("can't create client: %s\n", err)
		return
	}

	err = c.LogIn("", "", "")
	if err != nil {
		fmt.Printf("can't login: %s\n", err)
		return
	}

	ids := []string{"0403358"}
	langs := []string{"rus"}

	subs, err := c.IMDBSearchByID(ids, langs)
	if err != nil {
		fmt.Printf("can't search: %s\n", err)
		return
	}

	var testSubtitles Subtitles

	for _, sub := range subs {
		if sub.SubHash == "2717a7ef9e3346f9353dc4e340e7bcb7" {
			testSubtitles = append(testSubtitles, sub)
		}
	}

	subFiles, err := c.DownloadSubtitles(testSubtitles)
	if err != nil {
		fmt.Printf("can't download subtitles: %s\n", err)
		return
	}

	for i, sf := range subFiles {
		reader, err := sf.Reader()
		if err != nil {
			fmt.Printf("can't open reader: %s\n", err)
			return
		}
		scanner := bufio.NewScanner(reader)
		for j := 0; j < 21; j++ {
			if !scanner.Scan() {
				fmt.Printf("too few lines in subtitle file\n")
				return
			}
		}
		if scanner.Scan() {
			fmt.Printf("22nd line of subtitle %d: %s\n", i, scanner.Text())
		}
	}

	// Output:
	// 22nd line of subtitle 0: как звезд в небесах.
}


<|start_filename|>osdb.go<|end_filename|>
/*
Package osdb is an API client for opensubtitles.org

This is a client for the OSDb protocol. Currently the package only allows movie
identification, subtitles search, and download.
*/
package osdb

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"os"

	"github.com/kolo/xmlrpc"
)

const (
	// ChunkSize = 64k
	ChunkSize = 65536
)

// NewClient allocates a new OSDB client.
func NewClient() (*Client, error) {
	osdbServer := os.Getenv("OSDB_SERVER")
	if osdbServer == "" {
		osdbServer = DefaultOSDBServer
	}
	rpc, err := xmlrpc.NewClient(osdbServer, nil)
	if err != nil {
		return nil, err
	}

	c := &Client{
		UserAgent: DefaultUserAgent,
		Client:    rpc, // xmlrpc.Client
	}

	return c, nil
}

// HashFile generates an OSDB hash for an *os.File.
func HashFile(file *os.File) (hash uint64, err error) {
	fi, err := file.Stat()
	if err != nil {
		return
	}
	if fi.Size() < ChunkSize {
		return 0, fmt.Errorf("File is too small")
	}

	// Read head and tail blocks.
	buf := make([]byte, ChunkSize*2)
	err = readChunk(file, 0, buf[:ChunkSize])
	if err != nil {
		return
	}
	err = readChunk(file, fi.Size()-ChunkSize, buf[ChunkSize:])
	if err != nil {
		return
	}

	// Convert to uint64, and sum.
	var nums [(ChunkSize * 2) / 8]uint64
	reader := bytes.NewReader(buf)
	err = binary.Read(reader, binary.LittleEndian, &nums)
	if err != nil {
		return 0, err
	}
	for _, num := range nums {
		hash += num
	}

	return hash + uint64(fi.Size()), nil
}

// Hash generates an OSDB hash for a file.
func Hash(path string) (uint64, error) {
	file, err := os.Open(path)
	if err != nil {
		return 0, err
	}
	defer file.Close()
	return HashFile(file)
}

// Read a chunk of a file at `offset` so as to fill `buf`.
func readChunk(file *os.File, offset int64, buf []byte) (err error) {
	n, err := file.ReadAt(buf, offset)
	if err != nil {
		return
	}
	if n != ChunkSize {
		return fmt.Errorf("Invalid read %v", n)
	}
	return
}
