func MakePulsarRepositoryAddon() error { addonId := "repository.pulsar" addonName := "Pulsar Repository" pulsarHost := fmt.Sprintf("http://localhost:%d", config.ListenPort) addon := &xbmc.Addon{ Id: addonId, Name: addonName, Version: util.Version, ProviderName: config.Get().Info.Author, Extensions: []*xbmc.AddonExtension{ &xbmc.AddonExtension{ Point: "xbmc.addon.repository", Name: addonName, Info: &xbmc.AddonRepositoryInfo{ Text: pulsarHost + "/repository/steeve/plugin.video.pulsar/addons.xml", Compressed: false, }, Checksum: pulsarHost + "/repository/steeve/plugin.video.pulsar/addons.xml.md5", Datadir: &xbmc.AddonRepositoryDataDir{ Text: pulsarHost + "/repository/steeve/", Zip: true, }, }, &xbmc.AddonExtension{ Point: "xbmc.addon.metadata", Summaries: []*xbmc.AddonText{ &xbmc.AddonText{"Virtual repository for Pulsar Updates", "en"}, }, Platform: "all", }, }, } addonPath := filepath.Clean(filepath.Join(config.Get().Info.Path, "..", addonId)) if err := os.MkdirAll(addonPath, 0777); err != nil { return err } if err := copyFile(filepath.Join(config.Get().Info.Path, "icon.png"), filepath.Join(addonPath, "icon.png")); err != nil { return err } addonXmlFile, err := os.Create(filepath.Join(addonPath, "addon.xml")) if err != nil { return err } defer addonXmlFile.Close() return xml.NewEncoder(addonXmlFile).Encode(addon) }
func PopularShows(ctx *gin.Context) { genre := ctx.Params.ByName("genre") if genre == "0" { genre = "" } renderShows(tmdb.PopularShowsComplete(genre, config.Get().Language), ctx) }
func SearchMovies(ctx *gin.Context) { query := ctx.Request.URL.Query().Get("q") if query == "" { query = xbmc.Keyboard("", "Search Movies") } renderMovies(tmdb.SearchMovies(query, config.Get().Language), ctx) }
func ShowEpisodes(ctx *gin.Context) { show, err := tvdb.NewShowCached(ctx.Params.ByName("showId"), config.Get().Language) if err != nil { ctx.Error(err) return } seasonNumber, _ := strconv.Atoi(ctx.Params.ByName("season")) season := show.Seasons[seasonNumber] items := season.Episodes.ToListItems(show) for _, item := range items { item.Path = UrlForXBMC("/show/%d/season/%d/episode/%d/play", show.Id, season.Season, item.Info.Episode, ) item.ContextMenu = [][]string{ []string{"Choose stream...", fmt.Sprintf("XBMC.PlayMedia(%s)", UrlForXBMC("/show/%d/season/%d/episode/%d/links", show.Id, season.Season, item.Info.Episode, ))}, } item.IsPlayable = true } ctx.JSON(200, xbmc.NewView("episodes", items)) }
func (as *AddonSearcher) call(method string, searchObject interface{}) []*bittorrent.Torrent { torrents := make([]*bittorrent.Torrent, 0) cid, c := GetCallback() cbUrl := fmt.Sprintf("%s/callbacks/%s", util.GetHTTPHost(), cid) payload := &SearchPayload{ Method: method, CallbackURL: cbUrl, SearchObject: searchObject, } xbmc.ExecuteAddon(as.addonId, payload.String()) timeout := providerTimeout() conf := config.Get() if conf.CustomProviderTimeoutEnabled == true { timeout = time.Duration(conf.CustomProviderTimeout) * time.Second } select { case <-time.After(timeout): as.log.Info("Provider %s was too slow. Ignored.", as.addonId) RemoveCallback(cid) case result := <-c: json.Unmarshal(result, &torrents) } return torrents }
func TopRatedMovies(ctx *gin.Context) { genre := ctx.Params.ByName("genre") if genre == "0" { genre = "" } renderMovies(tmdb.TopRatedMoviesComplete(genre, config.Get().Language), ctx) }
func GetShow(showId int, language string) *Show { var show *Show cacheStore := cache.NewFileStore(path.Join(config.Get().ProfilePath, "cache")) key := fmt.Sprintf("com.tmdb.show.%d.%s", showId, language) if err := cacheStore.Get(key, &show); err != nil { rateLimiter.Call(func() { napping.Get( tmdbEndpoint+"tv/"+strconv.Itoa(showId), &napping.Params{"api_key": apiKey, "append_to_response": "credits,images,alternative_titles,translations,external_ids", "language": language}, &show, nil, ) }) if show != nil { cacheStore.Set(key, show, cacheTime) } } if show == nil { return nil } switch t := show.RawPopularity.(type) { case string: if popularity, err := strconv.ParseFloat(t, 64); err == nil { show.Popularity = popularity } case float64: show.Popularity = t } return show }
func getMovieById(movieId string, language string) *Movie { var movie *Movie cacheStore := cache.NewFileStore(path.Join(config.Get().ProfilePath, "cache")) key := fmt.Sprintf("com.tmdb.movie.%s.%s", movieId, language) if err := cacheStore.Get(key, &movie); err != nil { rateLimiter.Call(func() { napping.Get( tmdbEndpoint+"movie/"+movieId, &napping.Params{"api_key": apiKey, "append_to_response": "credits,images,alternative_titles,translations,external_ids,trailers", "language": language}, &movie, nil, ) if movie != nil { cacheStore.Set(key, movie, cacheTime) } }) } if movie == nil { return nil } switch t := movie.RawPopularity.(type) { case string: popularity, _ := strconv.ParseFloat(t, 64) movie.Popularity = popularity case float64: movie.Popularity = t } return movie }
func Migrate() { firstRun := filepath.Join(config.Get().Info.Path, ".firstrun") if _, err := os.Stat(firstRun); err == nil { return } file, _ := os.Create(firstRun) defer file.Close() log.Info("Preparing for first run") // Move ga client id file out of the cache directory gaFile := filepath.Join(config.Get().Info.Profile, "cache", "io.steeve.pulsar.ga") if _, err := os.Stat(gaFile); err == nil { os.Rename(gaFile, filepath.Join(config.Get().Info.Profile, "io.steeve.pulsar.ga")) } gaFile = filepath.Join(config.Get().Info.Profile, "io.steeve.pulsar.ga") if file, err := os.Open(gaFile); err == nil { if gzReader, err := gzip.NewReader(file); err != nil { outFile, _ := os.Create(gaFile + ".gz") gzWriter := gzip.NewWriter(outFile) file.Seek(0, os.SEEK_SET) io.Copy(gzWriter, file) gzWriter.Flush() gzWriter.Close() outFile.Close() file.Close() os.Rename(gaFile+".gz", gaFile) } else { gzReader.Close() } } // Remove the cache log.Info("Clearing cache") os.RemoveAll(filepath.Join(config.Get().Info.Profile, "cache")) log.Info("Creating Pulsar Repository Addon") if err := repository.MakePulsarRepositoryAddon(); err != nil { log.Error("Unable to create repository addon: %s", err) } }
func getClientId() string { clientId := "" key := "io.steeve.pulsar.ga" cacheStore := cache.NewFileStore(path.Join(config.Get().ProfilePath)) if err := cacheStore.Get(key, &clientId); err != nil { clientUUID, _ := uuid.NewV4() clientId := clientUUID.String() cacheStore.Set(key, clientId, clientIdCacheTime) } return clientId }
func MovieGenres(ctx *gin.Context) { genres := tmdb.GetMovieGenres(config.Get().Language) items := make(xbmc.ListItems, 0, len(genres)) for _, genre := range genres { items = append(items, &xbmc.ListItem{ Label: genre.Name, Path: UrlForXBMC("/movies/popular/%s", strconv.Itoa(genre.Id)), }) } ctx.JSON(200, xbmc.NewView("", items)) }
func movieLinks(imdbId string) []*bittorrent.Torrent { log.Println("Searching links for IMDB:", imdbId) movie := tmdb.GetMovieFromIMDB(imdbId, config.Get().Language) log.Printf("Resolved %s to %s\n", imdbId, movie.Title) searchers := providers.GetMovieSearchers() if len(searchers) == 0 { xbmc.Notify("Pulsar", "Unable to find any providers", config.AddonIcon()) } return providers.SearchMovie(searchers, movie) }
func NewShowCached(tvdbId string, language string) (*Show, error) { var show *Show cacheStore := cache.NewFileStore(path.Join(config.Get().ProfilePath, "cache")) key := fmt.Sprintf("com.tvdb.show.%s.%s", tvdbId, language) if err := cacheStore.Get(key, &show); err != nil { newShow, err := NewShow(tvdbId, language) if err != nil { return nil, err } if newShow != nil { cacheStore.Set(key, newShow, cacheTime) } show = newShow } return show, nil }
func TVIndex(ctx *gin.Context) { items := xbmc.ListItems{ {Label: "Search", Path: UrlForXBMC("/shows/search"), Thumbnail: config.AddonResource("img", "search.png")}, {Label: "Most Popular", Path: UrlForXBMC("/shows/popular"), Thumbnail: config.AddonResource("img", "popular.png")}, } for _, genre := range tmdb.GetTVGenres(config.Get().Language) { slug, _ := genreSlugs[genre.Id] items = append(items, &xbmc.ListItem{ Label: genre.Name, Path: UrlForXBMC("/shows/popular/%s", strconv.Itoa(genre.Id)), Thumbnail: config.AddonResource("img", fmt.Sprintf("genre_%s.png", slug)), }) } ctx.JSON(200, xbmc.NewView("", items)) }
func MoviesIndex(ctx *gin.Context) { items := xbmc.ListItems{ {Label: "Search", Path: UrlForXBMC("/movies/search"), Thumbnail: config.AddonResource("img", "search.png")}, {Label: "Most Popular", Path: UrlForXBMC("/movies/popular"), Thumbnail: config.AddonResource("img", "popular.png")}, {Label: "Top Rated", Path: UrlForXBMC("/movies/top"), Thumbnail: config.AddonResource("img", "top_rated.png")}, {Label: "Most Voted", Path: UrlForXBMC("/movies/mostvoted"), Thumbnail: config.AddonResource("img", "most_voted.png")}, {Label: "IMDB Top 250", Path: UrlForXBMC("/movies/imdb250"), Thumbnail: config.AddonResource("img", "imdb.png")}, } for _, genre := range tmdb.GetMovieGenres(config.Get().Language) { slug, _ := genreSlugs[genre.Id] items = append(items, &xbmc.ListItem{ Label: genre.Name, Path: UrlForXBMC("/movies/popular/%s", strconv.Itoa(genre.Id)), Thumbnail: config.AddonResource("img", fmt.Sprintf("genre_%s.png", slug)), }) } ctx.JSON(200, xbmc.NewView("", items)) }
func ShowSeasons(ctx *gin.Context) { show, err := tvdb.NewShowCached(ctx.Params.ByName("showId"), config.Get().Language) if err != nil { ctx.Error(err) return } items := show.Seasons.ToListItems(show) reversedItems := make(xbmc.ListItems, 0) for i := len(items) - 1; i >= 0; i-- { item := items[i] item.Path = UrlForXBMC("/show/%d/season/%d/episodes", show.Id, item.Info.Season) reversedItems = append(reversedItems, item) } // xbmc.ListItems always returns false to Less() so that order is unchanged ctx.JSON(200, xbmc.NewView("seasons", reversedItems)) }
func Find(externalId string, externalSource string) *FindResult { var result *FindResult cacheStore := cache.NewFileStore(path.Join(config.Get().ProfilePath, "cache")) key := fmt.Sprintf("com.tmdb.find.%s.%s", externalSource, externalId) if err := cacheStore.Get(key, &result); err != nil { rateLimiter.Call(func() { napping.Get( tmdbEndpoint+"find/"+externalId, &napping.Params{"api_key": apiKey, "external_source": externalSource}, &result, nil, ) cacheStore.Set(key, result, 365*24*time.Hour) }) } return result }
func showEpisodeLinks(showId string, seasonNumber, episodeNumber int) ([]*bittorrent.Torrent, error) { log.Println("Searching links for TVDB Id:", showId) show, err := tvdb.NewShowCached(showId, config.Get().Language) if err != nil { return nil, err } episode := show.Seasons[seasonNumber].Episodes[episodeNumber-1] log.Printf("Resolved %s to %s\n", showId, show.SeriesName) searchers := providers.GetEpisodeSearchers() if len(searchers) == 0 { xbmc.Notify("Pulsar", "Unable to find any providers", config.AddonIcon()) } return providers.SearchEpisode(searchers, show, episode), nil }
func Play(btService *bittorrent.BTService) gin.HandlerFunc { return func(ctx *gin.Context) { uri := ctx.Request.URL.Query().Get("uri") if uri == "" { return } torrent := bittorrent.NewTorrent(uri) magnet := torrent.Magnet() boosters := url.Values{ "tr": providers.DefaultTrackers, } magnet += "&" + boosters.Encode() player := bittorrent.NewBTPlayer(btService, magnet, config.Get().KeepFilesAfterStop == false) if player.Buffer() != nil { return } hostname := "localhost" if localIP, err := util.LocalIP(); err == nil { hostname = localIP.String() } rUrl, _ := url.Parse(fmt.Sprintf("http://%s:%d/files/%s", hostname, config.ListenPort, player.PlayURL())) ctx.Redirect(302, rUrl.String()) } }
func Routes(btService *bittorrent.BTService) *gin.Engine { r := gin.Default() gin.SetMode(gin.ReleaseMode) r.Use(ga.GATracker()) store := cache.NewFileStore(path.Join(config.Get().ProfilePath, "cache")) r.GET("/", Index) r.GET("/search", Search) r.GET("/pasted", PasteURL) movies := r.Group("/movies") { movies.GET("/", cache.Cache(store, IndexCacheTime), MoviesIndex) movies.GET("/search", SearchMovies) movies.GET("/popular", cache.Cache(store, DefaultCacheTime), PopularMovies) movies.GET("/popular/:genre", cache.Cache(store, DefaultCacheTime), PopularMovies) movies.GET("/top", cache.Cache(store, DefaultCacheTime), TopRatedMovies) movies.GET("/imdb250", cache.Cache(store, DefaultCacheTime), IMDBTop250) movies.GET("/mostvoted", cache.Cache(store, DefaultCacheTime), MoviesMostVoted) movies.GET("/genres", cache.Cache(store, IndexCacheTime), MovieGenres) } movie := r.Group("/movie") { movie.GET("/:imdbId/links", MovieLinks) movie.GET("/:imdbId/play", MoviePlay) } shows := r.Group("/shows") { shows.GET("/", cache.Cache(store, IndexCacheTime), TVIndex) shows.GET("/search", SearchShows) shows.GET("/popular", cache.Cache(store, DefaultCacheTime), PopularShows) shows.GET("/popular/:genre", cache.Cache(store, DefaultCacheTime), PopularShows) shows.GET("/top", cache.Cache(store, DefaultCacheTime), TopRatedShows) shows.GET("/mostvoted", cache.Cache(store, DefaultCacheTime), TVMostVoted) shows.GET("/genres", cache.Cache(store, IndexCacheTime), TVGenres) } show := r.Group("/show") { show.GET("/:showId/seasons", cache.Cache(store, DefaultCacheTime), ShowSeasons) show.GET("/:showId/season/:season/episodes", cache.Cache(store, EpisodesCacheTime), ShowEpisodes) show.GET("/:showId/season/:season/episode/:episode/links", ShowEpisodeLinks) show.GET("/:showId/season/:season/episode/:episode/play", ShowEpisodePlay) } provider := r.Group("/provider") { provider.GET("/:provider/movie/:imdbId", ProviderGetMovie) provider.GET("/:provider/show/:showId/season/:season/episode/:episode", ProviderGetEpisode) } repo := r.Group("/repository") { repo.GET("/:user/:repository/*filepath", repository.GetAddonFiles) repo.HEAD("/:user/:repository/*filepath", repository.GetAddonFiles) } r.GET("/youtube/:id", PlayYoutubeVideo) r.GET("/subtitles", SubtitlesIndex) r.GET("/subtitle/:id", SubtitleGet) r.GET("/play", Play(btService)) r.POST("/callbacks/:cid", providers.CallbackHandler) cmd := r.Group("/cmd") { cmd.GET("/clear_cache", ClearCache) } return r }
func UrlForXBMC(pattern string, args ...interface{}) string { u, _ := url.Parse(fmt.Sprintf(pattern, args...)) return "plugin://" + config.Get().Info.Id + u.String() }
func ClearCache(ctx *gin.Context) { os.RemoveAll(filepath.Join(config.Get().Info.Profile, "cache")) xbmc.Notify("Pulsar", "Cache cleared", config.AddonIcon()) }
func SubtitlesIndex(ctx *gin.Context) { q := ctx.Request.URL.Query() searchString := q.Get("searchstring") languages := strings.Split(q.Get("languages"), ",") labels := xbmc.InfoLabels( "VideoPlayer.Title", "VideoPlayer.OriginalTitle", "VideoPlayer.Year", "VideoPlayer.TVshowtitle", "VideoPlayer.Season", "VideoPlayer.Episode", ) playingFile := xbmc.PlayerGetPlayingFile() // are we reading a file from Pulsar? if strings.HasPrefix(playingFile, util.GetHTTPHost()) { playingFile = strings.Replace(playingFile, util.GetHTTPHost()+"/files", config.Get().DownloadPath, 1) playingFile, _ = url.QueryUnescape(playingFile) } for i, lang := range languages { if lang == "Portuguese (Brazil)" { languages[i] = "pob" } else { isoLang := xbmc.ConvertLanguage(lang, xbmc.ISO_639_2) if isoLang == "gre" { isoLang = "ell" } languages[i] = isoLang } } payloads := []osdb.SearchPayload{} if searchString != "" { payloads = append(payloads, osdb.SearchPayload{ Query: searchString, Languages: strings.Join(languages, ","), }) } else { if strings.HasPrefix(playingFile, "http://") == false && strings.HasPrefix(playingFile, "https://") == false { appendLocalFilePayloads(playingFile, &payloads) } if labels["VideoPlayer.TVshowtitle"] != "" { appendEpisodePayloads(labels, &payloads) } else { appendMoviePayloads(labels, &payloads) } } for i, payload := range payloads { payload.Languages = strings.Join(languages, ",") payloads[i] = payload } client, err := osdb.NewClient() if err != nil { ctx.AbortWithError(500, err) return } if err := client.LogIn("", "", ""); err != nil { ctx.AbortWithError(500, err) return } items := make(xbmc.ListItems, 0) results, _ := client.SearchSubtitles(payloads) for _, sub := range results { rating, _ := strconv.ParseFloat(sub.SubRating, 64) subLang := sub.LanguageName if subLang == "Brazilian" { subLang = "Portuguese (Brazil)" } item := &xbmc.ListItem{ Label: subLang, Label2: sub.SubFileName, Icon: strconv.Itoa(int((rating / 2) + 0.5)), Thumbnail: sub.ISO639, Path: UrlQuery(UrlForXBMC("/subtitle/%s", sub.IDSubtitleFile), "file", sub.SubFileName, "lang", sub.SubLanguageID, "fmt", sub.SubFormat, "dl", sub.SubDownloadLink), Properties: make(map[string]string), } if sub.MatchedBy == "moviehash" { item.Properties["sync"] = "true" } if sub.SubHearingImpaired == "1" { item.Properties["hearing_imp"] = "true" } items = append(items, item) } ctx.JSON(200, xbmc.NewView("", items)) }
func IMDBTop250(ctx *gin.Context) { renderMovies(tmdb.GetList("522effe419c2955e9922fcf3", config.Get().Language), ctx) }
func MoviesMostVoted(ctx *gin.Context) { renderMovies(tmdb.MostVotedMoviesComplete("", config.Get().Language), ctx) }
func main() { // Make sure we are properly multithreaded. runtime.GOMAXPROCS(runtime.NumCPU()) logging.SetFormatter(logging.MustStringFormatter("%{time:2006-01-02 15:04:05} %{level:.4s} %{module:-15s} %{message}")) logging.SetBackend(logging.NewLogBackend(os.Stdout, "", 0)) for _, line := range strings.Split(PulsarLogo, "\n") { log.Info(line) } log.Info("Version: %s Git: %s Go: %s", util.Version, util.GitCommit, runtime.Version()) conf := config.Reload() ensureSingleInstance() Migrate() xbmc.CloseAllDialogs() log.Info("Addon: %s v%s", conf.Info.Id, conf.Info.Version) btService := bittorrent.NewBTService(*makeBTConfiguration(conf)) var shutdown = func() { log.Info("Shutting down...") btService.Close() log.Info("Bye bye") os.Exit(0) } var watchParentProcess = func() { for { // did the parent die? shutdown! if os.Getppid() == 1 { log.Warning("Parent shut down. Me too.") go shutdown() break } time.Sleep(1 * time.Second) } } go watchParentProcess() http.Handle("/", api.Routes(btService)) http.Handle("/files/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.StripPrefix("/files/", http.FileServer(bittorrent.NewTorrentFS(btService, config.Get().DownloadPath))) handler.ServeHTTP(w, r) })) http.Handle("/reload", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { btService.Reconfigure(*makeBTConfiguration(config.Reload())) })) http.Handle("/shutdown", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { shutdown() })) xbmc.Notify("Pulsar", "Pulsar daemon has started", config.AddonIcon()) http.ListenAndServe(":"+strconv.Itoa(config.ListenPort), nil) }
func TopRatedShows(ctx *gin.Context) { renderShows(tmdb.TopRatedShowsComplete("", config.Get().Language), ctx) }