Compare commits

...

12 Commits

Author SHA1 Message Date
f2427ee6c7 Add a page to display a movie
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2021-08-22 10:39:18 -10:00
f8de3be779 Add a link from the torrent title to the show 2021-08-21 16:30:50 -10:00
7b960d6616 Update the movies' default page to trakttv trending 2021-08-21 16:10:54 -10:00
24324455da Download movie fanart and better torrent list on mobile
Show the thumb on wide screens and the fanart on small screens.
2021-08-21 13:20:52 -10:00
9488795186 Add a button to show the modules statuses
Some checks reported errors
continuous-integration/drone/push Build encountered an error
It's really annoying to wait for the modules to be loaded while visiting
the user profile or admin panel.
2021-08-21 10:45:55 -10:00
18c749f9e1 Remove unused modules 2021-08-20 15:47:21 -10:00
960ddcfa05 Handle embedded subtitles 2021-08-20 15:47:21 -10:00
04e3aee65b Fix indentation 2021-08-20 15:47:21 -10:00
d59c269caa Remove fresh
This package is not maintained.
2021-08-20 15:47:21 -10:00
36eeb6b983 Update the subtitles independently 2021-08-20 15:47:15 -10:00
7737e278c1 Fix user deletion 2021-08-12 16:37:00 -10:00
451fea7355 Update everything to work with node 16.X
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2021-08-12 16:05:17 -10:00
49 changed files with 12240 additions and 7970 deletions

View File

@ -3,12 +3,12 @@ name: default
steps: steps:
- name: prepare-workdir - name: prepare-workdir
image: alpine:3.13.1 image: alpine:3.14.3
commands: commands:
- mkdir canapeapp - mkdir canapeapp
- name: frontend - name: frontend
image: node:13.8.0 image: node:16.6.2
commands: commands:
- cd frontend - cd frontend
- npm install - npm install
@ -16,7 +16,7 @@ steps:
- npm run-script build - npm run-script build
- name: backend - name: backend
image: golang:1.15.7-alpine3.13 image: golang:1.16.7-alpine3.14
commands: commands:
- apk --no-cache add git - apk --no-cache add git
- GO111MODULE=off go get -tags 'postgres' -u github.com/golang-migrate/migrate/cmd/migrate - GO111MODULE=off go get -tags 'postgres' -u github.com/golang-migrate/migrate/cmd/migrate
@ -24,7 +24,7 @@ steps:
- CGO_ENABLED=0 go build -ldflags '-extldflags "-static"' -trimpath -v -o canapeapp/app backend/*.go - CGO_ENABLED=0 go build -ldflags '-extldflags "-static"' -trimpath -v -o canapeapp/app backend/*.go
- name: prepare-docker - name: prepare-docker
image: alpine:3.13.1 image: alpine:3.14.3
commands: commands:
- cp docker/run.sh canapeapp/run.sh - cp docker/run.sh canapeapp/run.sh
- cp migrate canapeapp/migrate - cp migrate canapeapp/migrate

View File

@ -141,7 +141,7 @@ func GetShows(env *web.Env, user *models.User, source string, category string, f
for _, id := range media.IDs { for _, id := range media.IDs {
pShow, _ := pShows.Has(id) pShow, _ := pShows.Has(id)
wShow, _ := wShows.IsShowInWishlist(id) wShow, _ := wShows.IsShowInWishlist(id)
show := shows.NewWithClient(&polochon.Show{ImdbID: id}, client, pShow, wShow) show := shows.NewWithClient(id, client, pShow, wShow)
// First check in the DB // First check in the DB
before := []polochon.Detailer{env.Backend.Detailer} before := []polochon.Detailer{env.Backend.Detailer}

View File

@ -1,6 +0,0 @@
root: .
valid_ext: .go
colors: 1
build_name: dev-build
build_log: dev-build.log
tmp_path: ../build

View File

@ -84,7 +84,6 @@ func FillEpisodeFromDB(eDB *episodeDB, pEpisode *polochon.ShowEpisode) {
pEpisode.Title = eDB.Title pEpisode.Title = eDB.Title
pEpisode.Rating = eDB.Rating pEpisode.Rating = eDB.Rating
pEpisode.Plot = eDB.Plot pEpisode.Plot = eDB.Plot
pEpisode.Thumb = eDB.Thumb
pEpisode.Runtime = eDB.Runtime pEpisode.Runtime = eDB.Runtime
pEpisode.Aired = eDB.Aired pEpisode.Aired = eDB.Aired
pEpisode.Thumb = imageURL(fmt.Sprintf( pEpisode.Thumb = imageURL(fmt.Sprintf(

View File

@ -90,6 +90,7 @@ func FillMovieFromDB(mDB *movieDB, pMovie *polochon.Movie) {
pMovie.SortTitle = mDB.SortTitle pMovie.SortTitle = mDB.SortTitle
pMovie.Tagline = mDB.Tagline pMovie.Tagline = mDB.Tagline
pMovie.Thumb = imageURL("movies/" + mDB.ImdbID + ".jpg") pMovie.Thumb = imageURL("movies/" + mDB.ImdbID + ".jpg")
pMovie.Fanart = imageURL("movies/" + mDB.ImdbID + "-fanart.jpg")
} }
// updateFromMovie will update the movieDB from a Movie // updateFromMovie will update the movieDB from a Movie

View File

@ -9,8 +9,9 @@ import (
// TorrentVideo reprensents a torrent embeding the video inforamtions // TorrentVideo reprensents a torrent embeding the video inforamtions
type TorrentVideo struct { type TorrentVideo struct {
*polochon.Torrent *polochon.Torrent
Img string `json:"img"` Thumb string `json:"thumb"`
Video polochon.Video `json:"video,omitempty"` Fanart string `json:"fanart"`
Video polochon.Video `json:"video,omitempty"`
} }
// NewTorrentVideo returns a new TorrentVideo // NewTorrentVideo returns a new TorrentVideo
@ -47,11 +48,13 @@ func (t *TorrentVideo) Update(detailer polochon.Detailer, db *sqlx.DB, log *logr
if err := GetShow(db, v.Show); err != nil { if err := GetShow(db, v.Show); err != nil {
return return
} }
t.Img = v.Show.Poster t.Fanart = v.Show.Fanart
t.Thumb = v.Show.Poster
v.Show = nil v.Show = nil
} }
case *polochon.Movie: case *polochon.Movie:
t.Img = v.Thumb t.Thumb = v.Thumb
t.Fanart = v.Fanart
} }
} }

View File

@ -2,20 +2,12 @@ package main
import ( import (
// Modules // Modules
_ "github.com/odwrtw/polochon/modules/addicted"
_ "github.com/odwrtw/polochon/modules/canape" _ "github.com/odwrtw/polochon/modules/canape"
_ "github.com/odwrtw/polochon/modules/eztv" _ "github.com/odwrtw/polochon/modules/eztv"
_ "github.com/odwrtw/polochon/modules/fsnotify"
_ "github.com/odwrtw/polochon/modules/imdb"
_ "github.com/odwrtw/polochon/modules/mock" _ "github.com/odwrtw/polochon/modules/mock"
_ "github.com/odwrtw/polochon/modules/openguessit"
_ "github.com/odwrtw/polochon/modules/opensubtitles"
_ "github.com/odwrtw/polochon/modules/pushover"
_ "github.com/odwrtw/polochon/modules/tmdb" _ "github.com/odwrtw/polochon/modules/tmdb"
_ "github.com/odwrtw/polochon/modules/tpb" _ "github.com/odwrtw/polochon/modules/tpb"
_ "github.com/odwrtw/polochon/modules/trakttv" _ "github.com/odwrtw/polochon/modules/trakttv"
_ "github.com/odwrtw/polochon/modules/transmission"
_ "github.com/odwrtw/polochon/modules/tvdb" _ "github.com/odwrtw/polochon/modules/tvdb"
_ "github.com/odwrtw/polochon/modules/yifysubtitles"
_ "github.com/odwrtw/polochon/modules/yts" _ "github.com/odwrtw/polochon/modules/yts"
) )

View File

@ -49,6 +49,51 @@ func PolochonMoviesHandler(env *web.Env, w http.ResponseWriter, r *http.Request)
return env.RenderJSON(w, movies) return env.RenderJSON(w, movies)
} }
// GetMovieHandler will return a single movie
func GetMovieHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
id := vars["id"]
user := auth.GetCurrentUser(r, env.Log)
client, err := user.NewPapiClient(env.Database)
if err != nil {
return env.RenderError(w, err)
}
movies, err := client.GetMovies()
if err != nil {
return env.RenderError(w, err)
}
moviesWishlist, err := models.GetMovieWishlist(env.Database, user.ID)
if err != nil {
return env.RenderError(w, err)
}
pMovie, _ := movies.Has(id)
movie := New(
id,
client,
pMovie,
moviesWishlist.IsMovieInWishlist(id),
)
detailers := []polochon.Detailer{env.Backend.Detailer}
err = movie.GetDetails(env, detailers)
if err != nil {
return env.RenderError(w, err)
}
torrenters := []polochon.Torrenter{env.Backend.Torrenter}
err = movie.GetTorrents(env, torrenters)
if err != nil {
env.Log.Errorf("error while getting movie torrents : %s", err)
}
return env.RenderJSON(w, movie)
}
// RefreshMovieHandler refreshes details for a movie // RefreshMovieHandler refreshes details for a movie
func RefreshMovieHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { func RefreshMovieHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
@ -283,6 +328,7 @@ func GetWishlistHandler(env *web.Env, w http.ResponseWriter, r *http.Request) er
func RefreshMovieSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { func RefreshMovieSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
lang := polochon.Language(vars["lang"])
// Get the user // Get the user
user := auth.GetCurrentUser(r, env.Log) user := auth.GetCurrentUser(r, env.Log)
@ -294,29 +340,35 @@ func RefreshMovieSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http.R
} }
movie := &papi.Movie{Movie: &polochon.Movie{ImdbID: id}} movie := &papi.Movie{Movie: &polochon.Movie{ImdbID: id}}
refreshSubs, err := client.UpdateSubtitles(movie) sub, err := client.UpdateSubtitle(movie, lang)
if err != nil { if err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }
subs := []subtitles.Subtitle{} // TODO: handle this with a better error
for _, lang := range refreshSubs { if sub == nil {
subtitleURL, _ := client.SubtitleURL(movie, lang) return env.RenderJSON(w, nil)
subs = append(subs, subtitles.Subtitle{
Language: lang,
URL: subtitleURL,
VVTFile: fmt.Sprintf("/movies/%s/subtitles/%s", id, lang),
})
} }
return env.RenderJSON(w, subs) url, err := client.DownloadURLWithToken(sub)
if err != nil {
return env.RenderError(w, err)
}
s := &subtitles.Subtitle{
Subtitle: sub.Subtitle,
URL: url,
VVTFile: fmt.Sprintf("/movies/%s/subtitles/%s", id, sub.Lang),
}
return env.RenderJSON(w, s)
} }
// DownloadVVTSubtitle returns a vvt subtitle for the movie // DownloadVVTSubtitle returns a vvt subtitle for the movie
func DownloadVVTSubtitle(env *web.Env, w http.ResponseWriter, r *http.Request) error { func DownloadVVTSubtitle(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
lang := vars["lang"] lang := polochon.Language(vars["lang"])
// Get the user // Get the user
user := auth.GetCurrentUser(r, env.Log) user := auth.GetCurrentUser(r, env.Log)
@ -327,7 +379,14 @@ func DownloadVVTSubtitle(env *web.Env, w http.ResponseWriter, r *http.Request) e
return env.RenderError(w, err) return env.RenderError(w, err)
} }
url, err := client.SubtitleURL(&papi.Movie{Movie: &polochon.Movie{ImdbID: id}}, lang) s := &papi.Subtitle{
Subtitle: &polochon.Subtitle{
Video: &papi.Movie{Movie: &polochon.Movie{ImdbID: id}},
Lang: lang,
},
}
url, err := client.DownloadURLWithToken(s)
if err != nil { if err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }

View File

@ -48,7 +48,7 @@ func (m *Movie) MarshalJSON() ([]byte, error) {
if m.pMovie != nil { if m.pMovie != nil {
// Get the DownloadURL // Get the DownloadURL
movieToMarshal.PolochonURL, _ = m.client.DownloadURL(m.pMovie) movieToMarshal.PolochonURL, _ = m.client.DownloadURLWithToken(m.pMovie)
// Get the metadata // Get the metadata
movieToMarshal.DateAdded = m.pMovie.DateAdded movieToMarshal.DateAdded = m.pMovie.DateAdded
@ -58,13 +58,14 @@ func (m *Movie) MarshalJSON() ([]byte, error) {
movieToMarshal.Container = m.pMovie.Container movieToMarshal.Container = m.pMovie.Container
// Append the Subtitles // Append the Subtitles
for _, l := range m.pMovie.Subtitles { for _, s := range m.pMovie.Subtitles {
subtitleURL, _ := m.client.SubtitleURL(m.pMovie, l) sub := subtitles.Subtitle{Subtitle: s.Subtitle}
movieToMarshal.Subtitles = append(movieToMarshal.Subtitles, subtitles.Subtitle{ if !sub.Embedded {
Language: l, subtitleURL, _ := m.client.DownloadURLWithToken(s)
URL: subtitleURL, sub.URL = subtitleURL
VVTFile: fmt.Sprintf("/movies/%s/subtitles/%s", m.ImdbID, l), sub.VVTFile = fmt.Sprintf("/movies/%s/subtitles/%s", m.ImdbID, s.Lang)
}) }
movieToMarshal.Subtitles = append(movieToMarshal.Subtitles, sub)
} }
} }
@ -73,13 +74,18 @@ func (m *Movie) MarshalJSON() ([]byte, error) {
// New returns a new Movie with all the needed infos // New returns a new Movie with all the needed infos
func New(imdbID string, client *papi.Client, pMovie *papi.Movie, isWishlisted bool) *Movie { func New(imdbID string, client *papi.Client, pMovie *papi.Movie, isWishlisted bool) *Movie {
var m *polochon.Movie
if pMovie != nil && pMovie.Movie != nil {
m = pMovie.Movie
} else {
m = &polochon.Movie{ImdbID: imdbID}
}
return &Movie{ return &Movie{
client: client, client: client,
pMovie: pMovie, pMovie: pMovie,
Wishlisted: isWishlisted, Wishlisted: isWishlisted,
Movie: &polochon.Movie{ Movie: m,
ImdbID: imdbID,
},
} }
} }
@ -144,7 +150,13 @@ func (m *Movie) Refresh(env *web.Env, detailers []polochon.Detailer) error {
} }
// Download poster // Download poster
err = web.Download(m.Thumb, m.imgFile(), true) err = web.Download(m.Thumb, m.imgFile("thumb"), 300)
if err != nil {
log.Errorf("got error trying to download the poster %q", err)
}
// Download fanart
err = web.Download(m.Fanart, m.imgFile("fanart"), 960)
if err != nil { if err != nil {
log.Errorf("got error trying to download the poster %q", err) log.Errorf("got error trying to download the poster %q", err)
} }
@ -231,13 +243,19 @@ func (m *Movie) RefreshTorrents(env *web.Env, torrenters []polochon.Torrenter) e
} }
// imgURL returns the default image url // imgURL returns the default image url
func (m *Movie) imgURL() string { func (m *Movie) imgURL(imgType string) string {
return fmt.Sprintf("movies/%s.jpg", m.ImdbID) var location string
if imgType == "thumb" {
location = m.ImdbID
} else {
location = m.ImdbID + "-" + imgType
}
return fmt.Sprintf("movies/%s.jpg", location)
} }
// imgFile returns the image location on disk // imgFile returns the image location on disk
func (m *Movie) imgFile() string { func (m *Movie) imgFile(imgType string) string {
return filepath.Join(models.PublicDir, "img", m.imgURL()) return filepath.Join(models.PublicDir, "img", m.imgURL(imgType))
} }
// getPolochonMovies returns an array of the user's polochon movies // getPolochonMovies returns an array of the user's polochon movies

View File

@ -41,9 +41,10 @@ func setupRoutes(env *web.Env) {
env.Handle("/movies/explore/options", extmedias.MovieExplorerOptions).WithRole(models.UserRole).Methods("GET") env.Handle("/movies/explore/options", extmedias.MovieExplorerOptions).WithRole(models.UserRole).Methods("GET")
env.Handle("/movies/search/{search}", movies.SearchMovie).WithRole(models.UserRole).Methods("GET") env.Handle("/movies/search/{search}", movies.SearchMovie).WithRole(models.UserRole).Methods("GET")
env.Handle("/movies/{id:tt[0-9]+}", movies.PolochonDeleteHandler).WithRole(models.UserRole).Methods("DELETE") env.Handle("/movies/{id:tt[0-9]+}", movies.PolochonDeleteHandler).WithRole(models.UserRole).Methods("DELETE")
env.Handle("/movies/{id:tt[0-9]+}", movies.GetMovieHandler).WithRole(models.UserRole).Methods("GET")
env.Handle("/movies/{id:tt[0-9]+}/refresh", movies.RefreshMovieHandler).WithRole(models.UserRole).Methods("POST") env.Handle("/movies/{id:tt[0-9]+}/refresh", movies.RefreshMovieHandler).WithRole(models.UserRole).Methods("POST")
env.Handle("/movies/{id:tt[0-9]+}/subtitles/{lang}", movies.DownloadVVTSubtitle).WithRole(models.UserRole).Methods("GET") env.Handle("/movies/{id:tt[0-9]+}/subtitles/{lang}", movies.DownloadVVTSubtitle).WithRole(models.UserRole).Methods("GET")
env.Handle("/movies/{id:tt[0-9]+}/subtitles/refresh", movies.RefreshMovieSubtitlesHandler).WithRole(models.UserRole).Methods("POST") env.Handle("/movies/{id:tt[0-9]+}/subtitles/{lang}", movies.RefreshMovieSubtitlesHandler).WithRole(models.UserRole).Methods("POST")
env.Handle("/movies/refresh", extmedias.RefreshMoviesHandler).WithRole(models.AdminRole).Methods("POST") env.Handle("/movies/refresh", extmedias.RefreshMoviesHandler).WithRole(models.AdminRole).Methods("POST")
// Shows routes // Shows routes
@ -54,7 +55,7 @@ func setupRoutes(env *web.Env) {
env.Handle("/shows/{id:tt[0-9]+}", shows.GetDetailsHandler).WithRole(models.UserRole).Methods("GET") env.Handle("/shows/{id:tt[0-9]+}", shows.GetDetailsHandler).WithRole(models.UserRole).Methods("GET")
env.Handle("/shows/{id:tt[0-9]+}/refresh", shows.RefreshShowHandler).WithRole(models.UserRole).Methods("POST") env.Handle("/shows/{id:tt[0-9]+}/refresh", shows.RefreshShowHandler).WithRole(models.UserRole).Methods("POST")
env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}", shows.RefreshEpisodeHandler).WithRole(models.UserRole).Methods("POST") env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}", shows.RefreshEpisodeHandler).WithRole(models.UserRole).Methods("POST")
env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}/subtitles/refresh", shows.RefreshEpisodeSubtitlesHandler).WithRole(models.UserRole).Methods("POST") env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}/subtitles/{lang}", shows.RefreshEpisodeSubtitlesHandler).WithRole(models.UserRole).Methods("POST")
env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}/subtitles/{lang}", shows.DownloadVVTSubtitle).WithRole(models.UserRole).Methods("GET") env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}/subtitles/{lang}", shows.DownloadVVTSubtitle).WithRole(models.UserRole).Methods("GET")
env.Handle("/shows/refresh", extmedias.RefreshShowsHandler).WithRole(models.AdminRole).Methods("POST") env.Handle("/shows/refresh", extmedias.RefreshShowsHandler).WithRole(models.AdminRole).Methods("POST")

View File

@ -3,7 +3,6 @@ package shows
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"time"
"git.quimbo.fr/odwrtw/canape/backend/models" "git.quimbo.fr/odwrtw/canape/backend/models"
"git.quimbo.fr/odwrtw/canape/backend/subtitles" "git.quimbo.fr/odwrtw/canape/backend/subtitles"
@ -26,18 +25,13 @@ func (e *Episode) MarshalJSON() ([]byte, error) {
var downloadURL string var downloadURL string
var subs []subtitles.Subtitle var subs []subtitles.Subtitle
var dateAdded time.Time
var quality string
var audioCodec string
var videoCodec string
var container string
// If the episode is present, fill the downloadURL // If the episode is present, fill the downloadURL
if e.show.pShow != nil { if e.show.pShow != nil {
pEpisode := e.show.pShow.GetEpisode(e.Season, e.Episode) pEpisode := e.show.pShow.GetEpisode(e.Season, e.Episode)
if pEpisode != nil { if pEpisode != nil {
// Get the DownloadURL // Get the DownloadURL
downloadURL, _ = e.show.client.DownloadURL( downloadURL, _ = e.show.client.DownloadURLWithToken(
&papi.Episode{ &papi.Episode{
ShowEpisode: &polochon.ShowEpisode{ ShowEpisode: &polochon.ShowEpisode{
ShowImdbID: e.ShowImdbID, ShowImdbID: e.ShowImdbID,
@ -46,20 +40,21 @@ func (e *Episode) MarshalJSON() ([]byte, error) {
}, },
}, },
) )
dateAdded = pEpisode.DateAdded
quality = string(pEpisode.Quality) e.ShowEpisode.VideoMetadata = pEpisode.ShowEpisode.VideoMetadata
audioCodec = pEpisode.AudioCodec e.ShowEpisode.File = pEpisode.ShowEpisode.File
videoCodec = pEpisode.VideoCodec
container = pEpisode.Container
// Append the Subtitles // Append the Subtitles
for _, l := range pEpisode.Subtitles { for _, s := range pEpisode.Subtitles {
subtitleURL, _ := e.show.client.SubtitleURL(pEpisode, l) sub := subtitles.Subtitle{Subtitle: s.Subtitle}
subs = append(subs, subtitles.Subtitle{ if !sub.Embedded {
Language: l, subtitleURL, _ := e.show.client.DownloadURLWithToken(s)
URL: subtitleURL, sub.URL = subtitleURL
VVTFile: fmt.Sprintf("/shows/%s/seasons/%d/episodes/%d/subtitles/%s", e.ShowImdbID, e.Season, e.Episode, l), sub.VVTFile = fmt.Sprintf(
}) "/shows/%s/seasons/%d/episodes/%d/subtitles/%s",
e.ShowImdbID, e.Season, e.Episode, s.Lang)
}
subs = append(subs, sub)
} }
} }
} }
@ -69,21 +64,11 @@ func (e *Episode) MarshalJSON() ([]byte, error) {
*alias *alias
PolochonURL string `json:"polochon_url"` PolochonURL string `json:"polochon_url"`
Subtitles []subtitles.Subtitle `json:"subtitles"` Subtitles []subtitles.Subtitle `json:"subtitles"`
DateAdded time.Time `json:"date_added"`
Quality string `json:"quality"`
AudioCodec string `json:"audio_codec"`
VideoCodec string `json:"video_codec"`
Container string `json:"container"`
Thumb string `json:"thumb"` Thumb string `json:"thumb"`
}{ }{
alias: (*alias)(e), alias: (*alias)(e),
PolochonURL: downloadURL, PolochonURL: downloadURL,
Subtitles: subs, Subtitles: subs,
DateAdded: dateAdded,
Quality: quality,
AudioCodec: audioCodec,
VideoCodec: videoCodec,
Container: container,
Thumb: e.Thumb, Thumb: e.Thumb,
} }

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log"
"strconv" "strconv"
"net/http" "net/http"
@ -35,15 +34,15 @@ func GetDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) err
pShow, err := client.GetShow(id) pShow, err := client.GetShow(id)
if err != nil && err != papi.ErrResourceNotFound { if err != nil && err != papi.ErrResourceNotFound {
log.Println("Got error getting show ", err) env.Log.Println("Got error getting show ", err)
} }
wShow, err := models.IsShowWishlisted(env.Database, user.ID, id) wShow, err := models.IsShowWishlisted(env.Database, user.ID, id)
if err != nil && err != papi.ErrResourceNotFound { if err != nil && err != papi.ErrResourceNotFound {
log.Println("Got error getting wishlisted show ", err) env.Log.Println("Got error getting wishlisted show ", err)
} }
s := NewWithClient(&polochon.Show{ImdbID: id}, client, pShow, wShow) s := NewWithClient(id, client, pShow, wShow)
// First try from the db // First try from the db
first := []polochon.Detailer{env.Backend.Detailer} first := []polochon.Detailer{env.Backend.Detailer}
// Then try from the polochon detailers // Then try from the polochon detailers
@ -81,15 +80,15 @@ func RefreshShowHandler(env *web.Env, w http.ResponseWriter, r *http.Request) er
pShow, err := client.GetShow(id) pShow, err := client.GetShow(id)
if err != nil && err != papi.ErrResourceNotFound { if err != nil && err != papi.ErrResourceNotFound {
log.Println("Got error getting show ", err) env.Log.Println("Got error getting show ", err)
} }
wShow, err := models.IsShowWishlisted(env.Database, user.ID, id) wShow, err := models.IsShowWishlisted(env.Database, user.ID, id)
if err != nil && err != papi.ErrResourceNotFound { if err != nil && err != papi.ErrResourceNotFound {
log.Println("Got error getting wishlisted show ", err) env.Log.Println("Got error getting wishlisted show ", err)
} }
s := NewWithClient(&polochon.Show{ImdbID: id}, client, pShow, wShow) s := NewWithClient(id, client, pShow, wShow)
// Refresh the polochon detailers // Refresh the polochon detailers
detailers := env.Config.Show.Detailers detailers := env.Config.Show.Detailers
err = s.Refresh(env, detailers) err = s.Refresh(env, detailers)
@ -163,7 +162,7 @@ func SearchShow(env *web.Env, w http.ResponseWriter, r *http.Request) error {
for _, s := range shows { for _, s := range shows {
pShow, _ := pShows.Has(s.ImdbID) pShow, _ := pShows.Has(s.ImdbID)
wShow, _ := wShows.IsShowInWishlist(s.ImdbID) wShow, _ := wShows.IsShowInWishlist(s.ImdbID)
show := NewWithClient(s, client, pShow, wShow) show := NewWithClient(s.ImdbID, client, pShow, wShow)
// First try from the db // First try from the db
first := []polochon.Detailer{env.Backend.Detailer} first := []polochon.Detailer{env.Backend.Detailer}
@ -242,8 +241,7 @@ func GetWishlistHandler(env *web.Env, w http.ResponseWriter, r *http.Request) er
showList := []*Show{} showList := []*Show{}
for _, wishedShow := range wShows.List() { for _, wishedShow := range wShows.List() {
pShow, _ := pShows.Has(wishedShow.ImdbID) pShow, _ := pShows.Has(wishedShow.ImdbID)
poloShow := &polochon.Show{ImdbID: wishedShow.ImdbID} show := NewWithClient(wishedShow.ImdbID, client, pShow, wishedShow)
show := NewWithClient(poloShow, client, pShow, wishedShow)
// First check in the DB // First check in the DB
before := []polochon.Detailer{env.Backend.Detailer} before := []polochon.Detailer{env.Backend.Detailer}
@ -309,7 +307,7 @@ func RefreshEpisodeHandler(env *web.Env, w http.ResponseWriter, r *http.Request)
} }
s := &Show{ s := &Show{
Show: &polochon.Show{ImdbID: id}, Show: pShow.Show,
client: client, client: client,
pShow: pShow, pShow: pShow,
} }
@ -343,6 +341,7 @@ func RefreshEpisodeHandler(env *web.Env, w http.ResponseWriter, r *http.Request)
func RefreshEpisodeSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { func RefreshEpisodeSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
lang := polochon.Language(vars["lang"])
// No need to check errors here as the router is making sure that season // No need to check errors here as the router is making sure that season
// and episode are numbers // and episode are numbers
@ -366,29 +365,35 @@ func RefreshEpisodeSubtitlesHandler(env *web.Env, w http.ResponseWriter, r *http
}, },
} }
refreshedSubs, err := client.UpdateSubtitles(e) sub, err := client.UpdateSubtitle(e, lang)
if err != nil { if err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }
subs := []subtitles.Subtitle{} // TODO: handle this with a better error
for _, lang := range refreshedSubs { if sub == nil {
subtitleURL, _ := client.SubtitleURL(e, lang) return env.RenderJSON(w, nil)
subs = append(subs, subtitles.Subtitle{
Language: lang,
URL: subtitleURL,
VVTFile: fmt.Sprintf("/shows/%s/seasons/%d/episodes/%d/subtitles/%s", e.ShowImdbID, e.Season, e.Episode, lang),
})
} }
return env.RenderJSON(w, subs) url, err := client.DownloadURL(sub)
if err != nil {
return env.RenderError(w, err)
}
s := &subtitles.Subtitle{
Subtitle: sub.Subtitle,
URL: url,
VVTFile: fmt.Sprintf("/shows/%s/seasons/%d/episodes/%d/subtitles/%s", e.ShowImdbID, e.Season, e.Episode, lang),
}
return env.RenderJSON(w, s)
} }
// DownloadVVTSubtitle returns a vvt subtitle for the movie // DownloadVVTSubtitle returns a vvt subtitle for the movie
func DownloadVVTSubtitle(env *web.Env, w http.ResponseWriter, r *http.Request) error { func DownloadVVTSubtitle(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
lang := vars["lang"] lang := polochon.Language(vars["lang"])
season, _ := strconv.Atoi(vars["season"]) season, _ := strconv.Atoi(vars["season"])
episode, _ := strconv.Atoi(vars["episode"]) episode, _ := strconv.Atoi(vars["episode"])
@ -401,13 +406,20 @@ func DownloadVVTSubtitle(env *web.Env, w http.ResponseWriter, r *http.Request) e
return env.RenderError(w, err) return env.RenderError(w, err)
} }
url, err := client.SubtitleURL(&papi.Episode{ s := &papi.Subtitle{
ShowEpisode: &polochon.ShowEpisode{ Subtitle: &polochon.Subtitle{
ShowImdbID: id, Video: &papi.Episode{
Season: season, ShowEpisode: &polochon.ShowEpisode{
Episode: episode, ShowImdbID: id,
Season: season,
Episode: episode,
},
},
Lang: lang,
}, },
}, lang) }
url, err := client.DownloadURLWithToken(s)
if err != nil { if err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }

View File

@ -62,7 +62,14 @@ func New(imdbID string) *Show {
} }
// NewWithClient returns a new Show with a polochon ShowConfig // NewWithClient returns a new Show with a polochon ShowConfig
func NewWithClient(show *polochon.Show, client *papi.Client, pShow *papi.Show, wShow *models.WishedShow) *Show { func NewWithClient(imdbID string, client *papi.Client, pShow *papi.Show, wShow *models.WishedShow) *Show {
var show *polochon.Show
if pShow == nil || pShow.Show == nil {
show = &polochon.Show{ImdbID: imdbID}
} else {
show = pShow.Show
}
s := &Show{ s := &Show{
Show: show, Show: show,
client: client, client: client,
@ -163,24 +170,24 @@ func (s *Show) downloadImages(env *web.Env) {
} }
// Download the show images // Download the show images
for _, img := range []struct { for _, img := range []struct {
url string url string
urlType string urlType string
scale bool maxWidth uint
}{ }{
{ {
url: s.Show.Banner, url: s.Show.Banner,
urlType: "banner", urlType: "banner",
scale: false, maxWidth: 0,
}, },
{ {
url: s.Show.Fanart, url: s.Show.Fanart,
urlType: "fanart", urlType: "fanart",
scale: false, maxWidth: 960,
}, },
{ {
url: s.Show.Poster, url: s.Show.Poster,
urlType: "poster", urlType: "poster",
scale: true, maxWidth: 300,
}, },
} { } {
if img.url == "" { if img.url == "" {
@ -190,7 +197,7 @@ func (s *Show) downloadImages(env *web.Env) {
if _, err := os.Stat(s.imgFile(img.urlType)); err == nil { if _, err := os.Stat(s.imgFile(img.urlType)); err == nil {
continue continue
} }
if err := web.Download(img.url, s.imgFile(img.urlType), img.scale); err != nil { if err := web.Download(img.url, s.imgFile(img.urlType), img.maxWidth); err != nil {
env.Log.Errorf("failed to dowload %s: %s", img.urlType, err) env.Log.Errorf("failed to dowload %s: %s", img.urlType, err)
} }
} }
@ -207,7 +214,7 @@ func (s *Show) downloadImages(env *web.Env) {
continue continue
} }
err := web.Download(e.Thumb, fileName, false) err := web.Download(e.Thumb, fileName, 0)
if err != nil { if err != nil {
env.Log.Errorf("failed to dowload the thumb for season %d episode %d ( %s ) : %s", e.Season, e.Episode, e.Thumb, err) env.Log.Errorf("failed to dowload the thumb for season %d episode %d ( %s ) : %s", e.Season, e.Episode, e.Thumb, err)
} }
@ -252,7 +259,7 @@ func getPolochonShows(env *web.Env, user *models.User) ([]*Show, error) {
// Create Shows objects from the shows retrieved // Create Shows objects from the shows retrieved
for _, pShow := range pshows.List() { for _, pShow := range pshows.List() {
wShow, _ := wShows.IsShowInWishlist(pShow.ImdbID) wShow, _ := wShows.IsShowInWishlist(pShow.ImdbID)
show := NewWithClient(&polochon.Show{ImdbID: pShow.ImdbID}, client, pShow, wShow) show := NewWithClient(pShow.ImdbID, client, pShow, wShow)
shows = append(shows, show) shows = append(shows, show)
} }
return shows, nil return shows, nil

View File

@ -1,8 +1,10 @@
package subtitles package subtitles
import polochon "github.com/odwrtw/polochon/lib"
// Subtitle represents a Subtitle // Subtitle represents a Subtitle
type Subtitle struct { type Subtitle struct {
Language string `json:"language"` *polochon.Subtitle
URL string `json:"url"` URL string `json:"url"`
VVTFile string `json:"vvt_file"` VVTFile string `json:"vvt_file"`
} }

View File

@ -13,7 +13,7 @@ import (
) )
// Download used for downloading file // Download used for downloading file
var Download = func(srcURL, dest string, scale bool) error { var Download = func(srcURL, dest string, maxWidth uint) error {
if err := createDirectory(dest); err != nil { if err := createDirectory(dest); err != nil {
return err return err
} }
@ -36,8 +36,8 @@ var Download = func(srcURL, dest string, scale bool) error {
return err return err
} }
if scale { if maxWidth != 0 {
image = resize.Resize(300, 0, image, resize.Lanczos3) image = resize.Resize(maxWidth, 0, image, resize.Lanczos3)
} }
// Create the file // Create the file

108
dev.sh
View File

@ -18,35 +18,35 @@ MIGRATION_SCHEMA=./migrations
DOCKER_COMPOSE_FILE=./docker/docker-compose.yml DOCKER_COMPOSE_FILE=./docker/docker-compose.yml
_usage() { _usage() {
prog=$(basename "$0") prog=$(basename "$0")
echo "Usage:" echo "Usage:"
echo " $prog back" echo " $prog back"
echo " Apply the migrations, build and run the backend" echo " Apply the migrations, build and run the backend"
echo "" echo ""
echo " $prog front" echo " $prog front"
echo " Install the JS packages and run the frontend" echo " Install the JS packages and run the frontend"
echo "" echo ""
echo " $prog migrate [args]" echo " $prog migrate [args]"
echo " Runs the migrate command with the given parameters" echo " Runs the migrate command with the given parameters"
echo "" echo ""
echo " $prog db-shell" echo " $prog db-shell"
echo " Get a psql shell on the database" echo " Get a psql shell on the database"
echo "" echo ""
echo " $prog db-init" echo " $prog db-init"
echo " Refresh the informations (imdb / movies / shows) needed" echo " Refresh the informations (imdb / movies / shows) needed"
echo " before the first run" echo " before the first run"
echo "" echo ""
echo " $prog docker-db [up|up -d|down|other docker-compose options...]" echo " $prog docker-db [up|up -d|down|other docker-compose options...]"
echo " Setup the database in a docker" echo " Setup the database in a docker"
exit 1 exit 1
} }
_log_info() { _log_info() {
printf "$(tput setaf 5)-->$(tput setaf 2) %s$(tput sgr0)\n" "$@" printf "$(tput setaf 5)-->$(tput setaf 2) %s$(tput sgr0)\n" "$@"
} }
_log_error() { _log_error() {
printf "$(tput setaf 6)-->$(tput setaf 9) %s$(tput sgr0)\n" "$@" printf "$(tput setaf 6)-->$(tput setaf 9) %s$(tput sgr0)\n" "$@"
} }
_check_command() { _check_command() {
@ -86,37 +86,37 @@ _check_command fresh || {
} }
canape_call() { canape_call() {
method=$1 method=$1
resource=$2 resource=$2
_log_info "Calling: $method $resource ..." _log_info "Calling: $method $resource ..."
response=$(curl --silent --show-error \ response=$(curl --silent --show-error \
-X "$method" \ -X "$method" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: $TOKEN" \ -H "Authorization: $TOKEN" \
"http://localhost:3000/$resource") "http://localhost:3000/$resource")
[ -n "$response" ] && echo "$response" [ -n "$response" ] && echo "$response"
_log_info "Done" _log_info "Done"
} }
canape_login() { canape_login() {
# Skip if the user defined its own token # Skip if the user defined its own token
[ -n "$CANAPE_TOKEN" ] && return [ -n "$CANAPE_TOKEN" ] && return
_log_info "Logging in ..." _log_info "Logging in ..."
json_data=$(jq -n -c --arg username "$CANAPE_USERNAME" \ json_data=$(jq -n -c --arg username "$CANAPE_USERNAME" \
--arg password "$CANAPE_PASS" \ --arg password "$CANAPE_PASS" \
'{username: $username,password: $password}') '{username: $username,password: $password}')
TOKEN=$(curl --silent --show-error -X POST \ TOKEN=$(curl --silent --show-error -X POST \
-d "$json_data" \ -d "$json_data" \
http://localhost:3000/users/login | jq -r .data.token ) http://localhost:3000/users/login | jq -r .data.token )
_log_info "Logged in" _log_info "Logged in"
} }
canape_logout() { canape_logout() {
# Skip if the user defined its own token # Skip if the user defined its own token
[ -n "$CANAPE_TOKEN" ] && return [ -n "$CANAPE_TOKEN" ] && return
_log_info "Disconnecting ..." _log_info "Disconnecting ..."
canape_call DELETE "users/tokens/$TOKEN" canape_call DELETE "users/tokens/$TOKEN"
_log_info "Disconnected" _log_info "Disconnected"
} }
case $1 in case $1 in
@ -135,7 +135,7 @@ case $1 in
# Apply the migrations # Apply the migrations
_migrate -path "$MIGRATION_SCHEMA" up _migrate -path "$MIGRATION_SCHEMA" up
(cd backend && CONFIG_FILE="../config.yml" fresh -c fresh.conf) (cd backend && CONFIG_FILE="../config.yml" go run ./*.go)
;; ;;
docker-db) docker-db)
_ensure_command docker _ensure_command docker
@ -147,15 +147,15 @@ case $1 in
(cd frontend && npm install && npm run-script start) (cd frontend && npm install && npm run-script start)
;; ;;
db-init) db-init)
_ensure_command jq _ensure_command jq
canape_login canape_login
canape_call POST ratings/refresh canape_call POST ratings/refresh
canape_call POST movies/refresh canape_call POST movies/refresh
canape_call POST shows/refresh canape_call POST shows/refresh
canape_logout canape_logout
;; ;;
*) *)
_log_error "Unknown command $1" _log_error "Unknown command $1"
_usage _usage
;; ;;
esac esac

View File

@ -20,10 +20,14 @@ export function updateUser(data) {
); );
} }
export function deleteUser(username) { export function deleteUser(username, userId) {
return request( return request(
"ADMIN_DELETE_USER", "ADMIN_DELETE_USER",
configureAxios().delete("/admins/users/" + username), configureAxios().delete("/admins/users/" + username),
[() => getUsers()] null,
{
username,
id: userId,
}
); );
} }

View File

@ -48,6 +48,17 @@ export function getMovieDetails(imdbId) {
); );
} }
export function fetchMovieDetails(imdbId) {
return request(
"MOVIE_FETCH_DETAILS",
configureAxios().get(`/movies/${imdbId}`),
null,
{
imdbId,
}
);
}
export function deleteMovie(imdbId, lastFetchUrl) { export function deleteMovie(imdbId, lastFetchUrl) {
return request("MOVIE_DELETE", configureAxios().delete(`/movies/${imdbId}`), [ return request("MOVIE_DELETE", configureAxios().delete(`/movies/${imdbId}`), [
fetchMovies(lastFetchUrl), fetchMovies(lastFetchUrl),

View File

@ -1,24 +1,25 @@
import { configureAxios, request } from "../requests"; import { configureAxios, request } from "../requests";
export const searchMovieSubtitles = (imdbId) => { export const searchMovieSubtitle = (imdbId, lang) => {
return request( return request(
"MOVIE_SUBTITLES_UPDATE", "MOVIE_SUBTITLES_UPDATE",
configureAxios().post(`/movies/${imdbId}/subtitles/refresh`), configureAxios().post(`/movies/${imdbId}/subtitles/${lang}`),
null, null,
{ imdbId: imdbId } { imdbId, lang }
); );
}; };
export const searchEpisodeSubtitles = (imdbId, season, episode) => { export const searchEpisodeSubtitle = (imdbId, season, episode, lang) => {
const url = `/shows/${imdbId}/seasons/${season}/episodes/${episode}`; const url = `/shows/${imdbId}/seasons/${season}/episodes/${episode}`;
return request( return request(
"EPISODE_SUBTITLES_UPDATE", "EPISODE_SUBTITLES_UPDATE",
configureAxios().post(`${url}/subtitles/refresh`), configureAxios().post(`${url}/subtitles/${lang}`),
null, null,
{ {
imdbId: imdbId, imdbId,
season: season, season,
episode: episode, episode,
lang,
} }
); );
}; };

View File

@ -14,7 +14,12 @@ import "../scss/app.scss";
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { Router, Route, Switch, Redirect } from "react-router-dom"; import {
HashRouter as Router,
Route,
Switch,
Redirect,
} from "react-router-dom";
import Container from "react-bootstrap/Container"; import Container from "react-bootstrap/Container";
// Auth // Auth
@ -28,6 +33,7 @@ import { AdminPanel } from "./components/admins/panel";
import { Notifications } from "./components/notifications/notifications"; import { Notifications } from "./components/notifications/notifications";
import { Alert } from "./components/alerts/alert"; import { Alert } from "./components/alerts/alert";
import MovieList from "./components/movies/list"; import MovieList from "./components/movies/list";
import { MovieDetails } from "./components/movies/details";
import { AppNavBar } from "./components/navbar"; import { AppNavBar } from "./components/navbar";
import { WsHandler } from "./components/websocket"; import { WsHandler } from "./components/websocket";
import { ShowDetails } from "./components/shows/details"; import { ShowDetails } from "./components/shows/details";
@ -61,6 +67,7 @@ const App = () => (
/> />
<Route path="/movies/polochon" exact component={MovieList} /> <Route path="/movies/polochon" exact component={MovieList} />
<Route path="/movies/wishlist" exact component={MovieList} /> <Route path="/movies/wishlist" exact component={MovieList} />
<Route path="/movies/details/:imdbId" exact component={MovieDetails} />
<Route path="/movies/search/:search" exact component={MovieList} /> <Route path="/movies/search/:search" exact component={MovieList} />
<Route <Route
path="/movies/explore/:source/:category" path="/movies/explore/:source/:category"
@ -76,7 +83,9 @@ const App = () => (
component={ShowList} component={ShowList}
/> />
<Route path="/shows/details/:imdbId" exact component={ShowDetails} /> <Route path="/shows/details/:imdbId" exact component={ShowDetails} />
<Route render={() => <Redirect to="/movies/explore/yts/seeds" />} /> <Route
render={() => <Redirect to="/movies/explore/trakttv/trending" />}
/>
</Switch> </Switch>
</Container> </Container>
</div> </div>

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import React from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { getAdminModules } from "../../actions/admins"; import { getAdminModules } from "../../actions/admins";
@ -10,9 +10,15 @@ export const AdminModules = () => {
const loading = useSelector((state) => state.admin.fetchingModules); const loading = useSelector((state) => state.admin.fetchingModules);
const modules = useSelector((state) => state.admin.modules); const modules = useSelector((state) => state.admin.modules);
useEffect(() => { const fetchModules = () => {
dispatch(getAdminModules()); dispatch(getAdminModules());
}, [dispatch]); };
return <Modules modules={modules} isLoading={loading} />; return (
<Modules
modules={modules}
isLoading={loading}
fetchModules={fetchModules}
/>
);
}; };

View File

@ -51,7 +51,7 @@ export const UserEdit = ({ id }) => {
e.preventDefault(); e.preventDefault();
} }
if (confirmDelete) { if (confirmDelete) {
dispatch(deleteUser(name)); dispatch(deleteUser(user.name, id));
setModal(false); setModal(false);
} else { } else {
setConfirmDelete(true); setConfirmDelete(true);

View File

@ -18,7 +18,7 @@ export const DownloadAndStream = ({ url, name, subtitles }) => {
DownloadAndStream.propTypes = { DownloadAndStream.propTypes = {
url: PropTypes.string, url: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
subtitles: PropTypes.array, subtitles: PropTypes.object,
}; };
const DownloadButton = ({ url }) => ( const DownloadButton = ({ url }) => (
@ -67,30 +67,29 @@ const StreamButton = ({ name, url, subtitles }) => {
StreamButton.propTypes = { StreamButton.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
subtitles: PropTypes.array, subtitles: PropTypes.object,
}; };
const Player = ({ url, subtitles }) => { const Player = ({ url, subtitles }) => {
const subs = subtitles || [];
return ( return (
<div className="embed-responsive embed-responsive-16by9"> <div className="embed-responsive embed-responsive-16by9">
<video className="embed-responsive-item" controls> <video className="embed-responsive-item" controls>
<source src={url} type="video/mp4" /> <source src={url} type="video/mp4" />
{subs.map((sub, index) => ( {subtitles &&
<track [...subtitles.entries()].map(([lang, sub]) => (
key={index} <track
kind="subtitles" key={lang}
label={sub.language} kind="subtitles"
src={sub.vvt_file} label={sub.language}
srcLang={sub.language} src={sub.vvt_file}
/> srcLang={sub.language}
))} />
))}
</video> </video>
</div> </div>
); );
}; };
Player.propTypes = { Player.propTypes = {
subtitles: PropTypes.array, subtitles: PropTypes.object,
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
}; };

View File

@ -3,25 +3,22 @@ import PropTypes from "prop-types";
import Dropdown from "react-bootstrap/Dropdown"; import Dropdown from "react-bootstrap/Dropdown";
import { prettySize, upperCaseFirst } from "../../utils";
export const SubtitlesButton = ({ export const SubtitlesButton = ({
subtitles, subtitles,
inLibrary, inLibrary,
searching,
search, search,
fetchingSubtitles,
}) => { }) => {
if (inLibrary === false) { if (inLibrary === false) {
return null; return null;
} }
/* eslint-disable */ /* eslint-disable */
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
/* eslint-enable */ /* eslint-enable */
const onSelect = (eventKey) => {
if (eventKey === null || eventKey != 1) {
setShow(false);
}
};
const onToggle = (isOpen, event, metadata) => { const onToggle = (isOpen, event, metadata) => {
// Don't close on select // Don't close on select
if (metadata && metadata.source !== "select") { if (metadata && metadata.source !== "select") {
@ -29,10 +26,20 @@ export const SubtitlesButton = ({
} }
}; };
const count = subtitles && subtitles.length !== 0 ? subtitles.length : 0; const searchAll = () => {
const langs = ["fr_FR", "en_US"];
for (const lang of langs) {
search(lang);
}
};
const count = subtitles && subtitles.size !== 0 ? subtitles.size : 0;
const searching = fetchingSubtitles.length > 0;
return ( return (
<span className="mr-1 mb-1"> <span className="mr-1 mb-1">
<Dropdown drop="up" show={show} onToggle={onToggle} onSelect={onSelect}> <Dropdown drop="up" show={show} onToggle={onToggle}>
<Dropdown.Toggle variant="secondary" bsPrefix="btn-sm w-md-100"> <Dropdown.Toggle variant="secondary" bsPrefix="btn-sm w-md-100">
<i className="fa fa-commenting mr-1" /> <i className="fa fa-commenting mr-1" />
Subtitles Subtitles
@ -40,9 +47,13 @@ export const SubtitlesButton = ({
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item eventKey={1} onClick={search}> <Dropdown.Item eventKey={1} onClick={searchAll}>
<i className={`fa ${searching ? "fa-spin" : ""} fa-refresh mr-1`} /> <div className="d-flex justify-content-between align-items-center">
Automatic search <span>Automatic search</span>
<div
className={`fa ${searching ? "fa-spin" : ""} fa-refresh ml-1`}
/>
</div>
</Dropdown.Item> </Dropdown.Item>
{count > 0 && ( {count > 0 && (
<React.Fragment> <React.Fragment>
@ -53,10 +64,13 @@ export const SubtitlesButton = ({
</React.Fragment> </React.Fragment>
)} )}
{count > 0 && {count > 0 &&
subtitles.map((subtitle, index) => ( [...subtitles.entries()].map(([lang, subtitle]) => (
<Dropdown.Item href={subtitle.url} key={index}> <SubtitleEntry
{subtitle.language.split("_")[1]} key={lang}
</Dropdown.Item> subtitle={subtitle}
searching={searching}
search={search}
/>
))} ))}
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
@ -64,8 +78,42 @@ export const SubtitlesButton = ({
); );
}; };
SubtitlesButton.propTypes = { SubtitlesButton.propTypes = {
subtitles: PropTypes.array, subtitles: PropTypes.object,
inLibrary: PropTypes.bool.isRequired, inLibrary: PropTypes.bool.isRequired,
searching: PropTypes.bool.isRequired, fetchingSubtitles: PropTypes.array.isRequired,
search: PropTypes.func.isRequired, search: PropTypes.func.isRequired,
}; };
export const SubtitleEntry = ({ subtitle, search }) => {
const lang = upperCaseFirst(subtitle.lang.split("_")[0]);
const size = subtitle.size ? subtitle.size : 0;
const embedded = subtitle.embedded ? subtitle.embedded : false;
const handleRefresh = () => {
search(subtitle.lang);
};
return (
<Dropdown.Item as="span" disabled={embedded}>
<div className="d-flex justify-content-between align-items-center">
<a href={subtitle.url ? subtitle.url : ""} className="link-unstyled">
{lang}
{embedded && <small className="ml-2">(Inside the video)</small>}
{size !== 0 && <span> ({prettySize(size)})</span>}
</a>
{!embedded && (
<div
onClick={handleRefresh}
className={`clickable fa ${
subtitle.searching ? "fa-spin" : ""
} fa-refresh`}
/>
)}
</div>
</Dropdown.Item>
);
};
SubtitleEntry.propTypes = {
search: PropTypes.func.isRequired,
subtitle: PropTypes.object,
};

View File

@ -1,8 +1,10 @@
import React from "react"; import React from "react";
import { useSelector } from "react-redux"; import PropTypes from "prop-types";
export const Fanart = () => { export const Fanart = ({ url }) => {
const url = useSelector((state) => state.show.show.fanart_url); if (url == "") {
return null;
}
return ( return (
<div className="show-fanart mx-n3 mt-n1"> <div className="show-fanart mx-n3 mt-n1">
<img <img
@ -12,3 +14,9 @@ export const Fanart = () => {
</div> </div>
); );
}; };
Fanart.propTypes = {
url: PropTypes.string.isRequired,
};
Fanart.defaultProps = {
url: "",
};

View File

@ -1,18 +1,23 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { prettySize } from "../../utils";
export const PolochonMetadata = ({ export const PolochonMetadata = ({
quality, quality,
container, container,
videoCodec, videoCodec,
audioCodec, audioCodec,
releaseGroup, releaseGroup,
size,
}) => { }) => {
if (!quality || quality === "") { if (!quality || quality === "") {
return null; return null;
} }
const metadata = [quality, container, videoCodec, audioCodec, releaseGroup] const s = size === 0 ? "" : prettySize(size);
const metadata = [quality, container, videoCodec, audioCodec, releaseGroup, s]
.filter((m) => m && m !== "") .filter((m) => m && m !== "")
.join(", "); .join(", ");
@ -29,4 +34,5 @@ PolochonMetadata.propTypes = {
videoCodec: PropTypes.string, videoCodec: PropTypes.string,
audioCodec: PropTypes.string, audioCodec: PropTypes.string,
releaseGroup: PropTypes.string, releaseGroup: PropTypes.string,
size: PropTypes.number,
}; };

View File

@ -53,6 +53,7 @@ const ListDetails = (props) => {
container={props.data.container} container={props.data.container}
audioCodec={props.data.audio_codec} audioCodec={props.data.audio_codec}
videoCodec={props.data.video_codec} videoCodec={props.data.video_codec}
size={props.data.size}
/> />
<Plot plot={props.data.plot} /> <Plot plot={props.data.plot} />
{props.children} {props.children}

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import Loader from "../loader/loader"; import Loader from "../loader/loader";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
@ -7,11 +7,29 @@ import { upperCaseFirst } from "../../utils";
// TODO: udpate this // TODO: udpate this
import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { OverlayTrigger, Tooltip } from "react-bootstrap";
const Modules = ({ isLoading, modules }) => { const Modules = ({ isLoading, modules, fetchModules }) => {
const [show, setShow] = useState(false);
if (isLoading) { if (isLoading) {
return <Loader />; return <Loader />;
} }
const handleClick = () => {
fetchModules();
setShow(true);
};
if (!show) {
return (
<div className="row">
<div className="col-12 col-md-8 offset-md-2 mb-3">
<div className="btn btn-secondary w-100" onClick={handleClick}>
Show modules status
</div>
</div>
</div>
);
}
return ( return (
<div className="row"> <div className="row">
{Object.keys(modules).map((type) => ( {Object.keys(modules).map((type) => (
@ -23,6 +41,7 @@ const Modules = ({ isLoading, modules }) => {
Modules.propTypes = { Modules.propTypes = {
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
modules: PropTypes.object.isRequired, modules: PropTypes.object.isRequired,
fetchModules: PropTypes.func.isRequired,
}; };
export default Modules; export default Modules;

View File

@ -0,0 +1,162 @@
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { useSelector, useDispatch } from "react-redux";
import {
fetchMovieDetails,
getMovieDetails,
movieWishlistToggle,
} from "../../actions/movies";
import { searchMovieSubtitle } from "../../actions/subtitles";
import Loader from "../loader/loader";
import { Fanart } from "../details/fanart";
import { Plot } from "../details/plot";
import { Rating } from "../details/rating";
import { ReleaseDate } from "../details/releaseDate";
import { Title } from "../details/title";
import { PolochonMetadata } from "../details/polochon";
import { TrackingLabel } from "../details/tracking";
import { Genres } from "../details/genres";
import { Runtime } from "../details/runtime";
import { DownloadAndStream } from "../buttons/download";
import { ImdbBadge } from "../buttons/imdb";
import { TorrentsButton } from "../buttons/torrents";
import { SubtitlesButton } from "../buttons/subtitles";
import { ShowMore } from "../buttons/showMore";
export const MovieDetails = ({ match }) => {
const dispatch = useDispatch();
const loading = useSelector((state) => state.movie.loading);
const fanartUrl = useSelector((state) => state.movie.movie.fanart);
useEffect(() => {
dispatch(fetchMovieDetails(match.params.imdbId));
}, [dispatch, match]);
if (loading) {
return <Loader />;
}
return (
<React.Fragment>
<Fanart url={fanartUrl} />
<Header />
</React.Fragment>
);
};
MovieDetails.propTypes = {
match: PropTypes.object.isRequired,
};
export const Header = () => {
const dispatch = useDispatch();
const {
audioCodec,
container,
fetchingDetails,
fetchingSubtitles,
genres,
imdb_id: imdbId,
plot,
polochon_url: polochonUrl,
poster_url: posterUrl,
quality,
rating,
runtime,
size,
subtitles,
title,
torrents,
videoCodec,
votes,
wishlisted,
year,
release_group: releaseGroup,
} = useSelector((state) => state.movie.movie);
const inLibrary = polochonUrl !== "";
if (!imdbId || imdbId === "") {
return null;
}
return (
<div className="col-12 col-lg-10 offset-lg-1 mb-3">
<div className="d-flex flex-column flex-md-row">
<div className="d-flex justify-content-center">
<img className="overflow-hidden object-fit-cover" src={posterUrl} />
</div>
<div className="ml-sm-1">
<div className="card-body">
<p className="card-title">
<Title
title={title}
wishlisted={wishlisted}
wishlist={() =>
dispatch(movieWishlistToggle(imdbId, wishlisted))
}
/>
</p>
<p className="card-text">
<ReleaseDate date={year} />
</p>
<p className="card-text">
<Runtime runtime={runtime} />
</p>
<p className="card-text">
<Genres genres={genres} />
</p>
<p className="card-text">
<Rating rating={rating} votes={votes} />
</p>
<div className="card-text">
<ImdbBadge imdbId={imdbId} />
<DownloadAndStream
url={polochonUrl}
name={title}
subtitles={subtitles}
/>
</div>
<p className="card-text">
<TrackingLabel inLibrary={inLibrary} wishlisted={wishlisted} />
</p>
<p className="card-text">
<PolochonMetadata
quality={quality}
releaseGroup={releaseGroup}
container={container}
audioCodec={audioCodec}
videoCodec={videoCodec}
size={size}
/>
</p>
<p className="card-text">
<Plot plot={plot} />
</p>
<div className="card-text">
<ShowMore id={imdbId} inLibrary={inLibrary}>
<TorrentsButton
torrents={torrents}
searching={fetchingDetails}
search={() => dispatch(getMovieDetails(imdbId))}
url={`#/torrents/search/movies/${encodeURI(title)}`}
/>
<SubtitlesButton
inLibrary={inLibrary}
fetchingSubtitles={fetchingSubtitles}
subtitles={subtitles}
search={(lang) => dispatch(searchMovieSubtitle(imdbId, lang))}
/>
</ShowMore>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -37,7 +37,7 @@ const fetchUrl = (match) => {
} }
}; };
const MovieList = ({ match }) => { const MovieList = ({ match, history }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
@ -63,6 +63,10 @@ const MovieList = ({ match }) => {
[dispatch] [dispatch]
); );
const movieDetails = (imdbId) => {
history.push("/movies/details/" + imdbId);
};
return ( return (
<div className="row"> <div className="row">
<ListPosters <ListPosters
@ -73,8 +77,8 @@ const MovieList = ({ match }) => {
exploreOptions={exploreOptions} exploreOptions={exploreOptions}
selectedImdbId={selectedImdbId} selectedImdbId={selectedImdbId}
onClick={selectFunc} onClick={selectFunc}
onDoubleClick={() => {}} onDoubleClick={movieDetails}
onKeyEnter={() => {}} onKeyEnter={movieDetails}
params={match.params} params={match.params}
loading={loading} loading={loading}
/> />
@ -105,6 +109,7 @@ MovieList.propTypes = {
updateFilter: PropTypes.func, updateFilter: PropTypes.func,
movieWishlistToggle: PropTypes.func, movieWishlistToggle: PropTypes.func,
selectMovie: PropTypes.func, selectMovie: PropTypes.func,
history: PropTypes.object,
match: PropTypes.object, match: PropTypes.object,
}; };

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { searchMovieSubtitles } from "../../actions/subtitles"; import { searchMovieSubtitle } from "../../actions/subtitles";
import { SubtitlesButton } from "../buttons/subtitles"; import { SubtitlesButton } from "../buttons/subtitles";
@ -15,16 +15,16 @@ export const MovieSubtitlesButton = () => {
const subtitles = useSelector( const subtitles = useSelector(
(state) => state.movies.movies.get(imdbId).subtitles (state) => state.movies.movies.get(imdbId).subtitles
); );
const searching = useSelector( const fetchingSubtitles = useSelector(
(state) => state.movies.movies.get(imdbId).fetchingSubtitles (state) => state.movies.movies.get(imdbId).fetchingSubtitles
); );
return ( return (
<SubtitlesButton <SubtitlesButton
inLibrary={inLibrary} inLibrary={inLibrary}
searching={searching} fetchingSubtitles={fetchingSubtitles}
subtitles={subtitles} subtitles={subtitles}
search={() => dispatch(searchMovieSubtitles(imdbId))} search={(lang) => dispatch(searchMovieSubtitle(imdbId, lang))}
/> />
); );
}; };

View File

@ -99,7 +99,7 @@ Search.propTypes = {
const MoviesDropdown = () => ( const MoviesDropdown = () => (
<NavDropdown title="Movies" id="navbar-movies-dropdown"> <NavDropdown title="Movies" id="navbar-movies-dropdown">
<LinkContainer to="/movies/explore/yts/seeds"> <LinkContainer to="/movies/explore/trakttv/trending">
<NavDropdown.Item>Discover</NavDropdown.Item> <NavDropdown.Item>Discover</NavDropdown.Item>
</LinkContainer> </LinkContainer>
<LinkContainer to="/movies/polochon"> <LinkContainer to="/movies/polochon">

View File

@ -4,7 +4,7 @@ import { useSelector, useDispatch } from "react-redux";
import Loader from "../loader/loader"; import Loader from "../loader/loader";
import { Fanart } from "./details/fanart"; import { Fanart } from "../details/fanart";
import { Header } from "./details/header"; import { Header } from "./details/header";
import { SeasonsList } from "./details/seasons"; import { SeasonsList } from "./details/seasons";
@ -13,6 +13,7 @@ import { fetchShowDetails } from "../../actions/shows";
export const ShowDetails = ({ match }) => { export const ShowDetails = ({ match }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const loading = useSelector((state) => state.show.loading); const loading = useSelector((state) => state.show.loading);
const fanartUrl = useSelector((state) => state.show.show.fanart_url);
useEffect(() => { useEffect(() => {
dispatch(fetchShowDetails(match.params.imdbId)); dispatch(fetchShowDetails(match.params.imdbId));
@ -24,7 +25,7 @@ export const ShowDetails = ({ match }) => {
return ( return (
<React.Fragment> <React.Fragment>
<Fanart /> <Fanart url={fanartUrl} />
<div className="row no-gutters"> <div className="row no-gutters">
<Header /> <Header />
<SeasonsList /> <SeasonsList />

View File

@ -74,6 +74,7 @@ export const Episode = ({ season, episode }) => {
container={data.container} container={data.container}
audioCodec={data.audio_codec} audioCodec={data.audio_codec}
videoCodec={data.video_codec} videoCodec={data.video_codec}
size={data.size}
light light
/> />
<ShowMore <ShowMore

View File

@ -2,7 +2,7 @@ import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { searchEpisodeSubtitles } from "../../../actions/subtitles"; import { searchEpisodeSubtitle } from "../../../actions/subtitles";
import { SubtitlesButton } from "../../buttons/subtitles"; import { SubtitlesButton } from "../../buttons/subtitles";
@ -10,30 +10,27 @@ export const EpisodeSubtitlesButton = ({ season, episode }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const imdbId = useSelector((state) => state.show.show.imdb_id); const imdbId = useSelector((state) => state.show.show.imdb_id);
const searching = useSelector((state) => const fetchingSubtitles = useSelector(
state.show.show.seasons.get(season).get(episode).fetchingSubtitles (state) =>
? state.show.show.seasons.get(season).get(episode).fetchingSubtitles state.show.show.seasons.get(season).get(episode).fetchingSubtitles
: false
); );
const inLibrary = useSelector( const inLibrary = useSelector(
(state) => (state) =>
state.show.show.seasons.get(season).get(episode).polochon_url !== "" state.show.show.seasons.get(season).get(episode).polochon_url !== ""
); );
const subtitles = useSelector((state) => const subtitles = useSelector(
state.show.show.seasons.get(season).get(episode).subtitles (state) => state.show.show.seasons.get(season).get(episode).subtitles
? state.show.show.seasons.get(season).get(episode).subtitles
: []
); );
const search = () => { const search = (lang) => {
dispatch(searchEpisodeSubtitles(imdbId, season, episode)); dispatch(searchEpisodeSubtitle(imdbId, season, episode, lang));
}; };
return ( return (
<SubtitlesButton <SubtitlesButton
subtitles={subtitles} subtitles={subtitles}
inLibrary={inLibrary} inLibrary={inLibrary}
searching={searching} fetchingSubtitles={fetchingSubtitles}
search={search} search={search}
/> />
); );

View File

@ -1,17 +1,27 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
export const Poster = ({ url }) => { export const Poster = ({ thumb, fanart }) => {
if (!url || url === "") { if (thumb === "" && fanart === "") {
return null; return null;
} }
return ( return (
<div className="col-md-2 d-none d-md-block"> <div className="col-md-2">
<img className="card-img" src={url} /> {thumb !== "" && (
<img className="card-img d-none d-sm-block" src={thumb} />
)}
{fanart !== "" && (
<img className="card-img d-block d-sm-none" src={fanart} />
)}
</div> </div>
); );
}; };
Poster.propTypes = { Poster.propTypes = {
url: PropTypes.string, thumb: PropTypes.string,
fanart: PropTypes.string,
};
Poster.defaultProps = {
thumb: "",
fanart: "",
}; };

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { Torrent } from "./torrent"; import { Torrent } from "./torrent";
import { Poster } from "./poster"; import { Poster } from "./poster";
@ -17,18 +18,32 @@ export const TorrentGroup = ({ torrentKey }) => {
const title = (torrent) => { const title = (torrent) => {
switch (torrent.type) { switch (torrent.type) {
case "movie": case "movie":
return torrent.video.title; return (
<Link
className="link-unstyled"
to={`/movies/details/${torrent.video.imdb_id}`}
>
{torrent.video.title}
</Link>
);
case "episode": case "episode":
return torrent.video.show_title; return (
<Link
className="link-unstyled"
to={`/shows/details/${torrent.video.show_imdb_id}`}
>
{torrent.video.show_title}
</Link>
);
default: default:
return "Files"; return <span>Files</span>;
} }
}; };
return ( return (
<div className="w-100 mb-3 card"> <div className="w-100 mb-3 card">
<div className="row no-gutters"> <div className="row no-gutters">
<Poster url={torrents[0].img} /> <Poster thumb={torrents[0].thumb} fanart={torrents[0].fanart} />
<div className="col-sm"> <div className="col-sm">
<div className="card-body"> <div className="card-body">
<h4 className="card-title">{title(torrents[0])}</h4> <h4 className="card-title">{title(torrents[0])}</h4>

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import React from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { PolochonList } from "../polochons/list"; import { PolochonList } from "../polochons/list";
@ -12,15 +12,19 @@ export const UserProfile = () => {
const modules = useSelector((state) => state.user.modules); const modules = useSelector((state) => state.user.modules);
const modulesLoading = useSelector((state) => state.user.modulesLoading); const modulesLoading = useSelector((state) => state.user.modulesLoading);
useEffect(() => { const fetchModules = () => {
dispatch(getUserModules()); dispatch(getUserModules());
}, [dispatch]); };
return ( return (
<div> <div>
<UserEdit /> <UserEdit />
<PolochonList /> <PolochonList />
<Modules modules={modules} isLoading={modulesLoading} /> <Modules
modules={modules}
isLoading={modulesLoading}
fetchModules={fetchModules}
/>
</div> </div>
); );
}; };

View File

@ -17,6 +17,11 @@ export default (state = defaultState, action) =>
break; break;
} }
case "ADMIN_DELETE_USER_FULFILLED": {
draft.users.delete(action.payload.main.id);
break;
}
case "ADMIN_GET_STATS_FULFILLED": { case "ADMIN_GET_STATS_FULFILLED": {
draft.stats = action.payload.response.data; draft.stats = action.payload.response.data;
break; break;

View File

@ -5,6 +5,7 @@ import { enableMapSet } from "immer";
enableMapSet(); enableMapSet();
import movies from "./movies"; import movies from "./movies";
import movie from "./movie";
import shows from "./shows"; import shows from "./shows";
import show from "./show"; import show from "./show";
import user from "./users"; import user from "./users";
@ -16,6 +17,7 @@ import notifications from "./notifications";
export default combineReducers({ export default combineReducers({
movies, movies,
movie,
shows, shows,
show, show,
user, user,

View File

@ -0,0 +1,77 @@
import { produce } from "immer";
import { formatSubtitle, formatMovie } from "./utils";
const defaultState = {
loading: false,
movie: {},
};
export default (state = defaultState, action) =>
produce(state, (draft) => {
switch (action.type) {
case "MOVIE_FETCH_DETAILS_PENDING":
draft.loading = true;
break;
case "MOVIE_FETCH_DETAILS_FULFILLED": {
draft.movie = formatMovie(action.payload.response.data);
draft.loading = false;
break;
}
case "MOVIE_GET_DETAILS_PENDING": {
let imdbId = action.payload.main.imdbId;
if (draft.movie.imdb_id !== imdbId) {
break;
}
draft.movie.fetchingDetails = true;
break;
}
case "MOVIE_GET_DETAILS_FULFILLED": {
let imdbId = action.payload.main.imdbId;
if (draft.movie.imdb_id !== imdbId) {
break;
}
draft.movie = formatMovie(action.payload.response.data);
break;
}
case "MOVIE_SUBTITLES_UPDATE_PENDING": {
let imdbId = action.payload.main.imdbId;
if (draft.movie.imdb_id !== imdbId) {
break;
}
let lang = action.payload.main.lang;
draft.movie.fetchingSubtitles.push(lang);
if (draft.movie.subtitles.get(lang)) {
draft.movie.subtitles.get(lang).searching = true;
}
break;
}
case "MOVIE_SUBTITLES_UPDATE_FULFILLED": {
let imdbId = action.payload.main.imdbId;
if (draft.movie.imdb_id !== imdbId) {
break;
}
let lang = action.payload.main.lang;
let data = action.payload.response.data;
draft.movie.fetchingSubtitles = draft.movie.fetchingSubtitles.filter(
(l) => l != lang
);
if (data) {
draft.movie.subtitles.set(lang, formatSubtitle(data));
}
break;
}
default:
return draft;
}
});

View File

@ -1,6 +1,6 @@
import { produce } from "immer"; import { produce } from "immer";
import { formatTorrents } from "../utils"; import { formatSubtitle, formatMovie } from "./utils";
const defaultState = { const defaultState = {
loading: false, loading: false,
@ -10,13 +10,6 @@ const defaultState = {
exploreOptions: {}, exploreOptions: {},
}; };
const formatMovie = (movie) => {
movie.fetchingDetails = false;
movie.fetchingSubtitles = false;
movie.torrents = formatTorrents(movie);
return movie;
};
const formatMovies = (movies = []) => { const formatMovies = (movies = []) => {
let allMoviesInPolochon = true; let allMoviesInPolochon = true;
movies.map((movie) => { movies.map((movie) => {
@ -64,10 +57,17 @@ export default (state = defaultState, action) =>
break; break;
case "MOVIE_GET_DETAILS_PENDING": case "MOVIE_GET_DETAILS_PENDING":
if (!draft.movies.get(action.payload.main.imdbId)) {
break;
}
draft.movies.get(action.payload.main.imdbId).fetchingDetails = true; draft.movies.get(action.payload.main.imdbId).fetchingDetails = true;
break; break;
case "MOVIE_GET_DETAILS_FULFILLED": case "MOVIE_GET_DETAILS_FULFILLED":
if (!draft.movies.get(action.payload.main.imdbId)) {
break;
}
draft.movies.set( draft.movies.set(
action.payload.response.data.imdb_id, action.payload.response.data.imdb_id,
formatMovie(action.payload.response.data) formatMovie(action.payload.response.data)
@ -87,15 +87,36 @@ export default (state = defaultState, action) =>
draft.lastFetchUrl = action.payload.url; draft.lastFetchUrl = action.payload.url;
break; break;
case "MOVIE_SUBTITLES_UPDATE_PENDING": case "MOVIE_SUBTITLES_UPDATE_PENDING": {
draft.movies.get(action.payload.main.imdbId).fetchingSubtitles = true; let imdbId = action.payload.main.imdbId;
break; let lang = action.payload.main.lang;
if (!draft.movies.get(imdbId)) {
break;
}
case "MOVIE_SUBTITLES_UPDATE_FULFILLED": draft.movies.get(imdbId).fetchingSubtitles.push(lang);
draft.movies.get(action.payload.main.imdbId).fetchingSubtitles = false; if (draft.movies.get(imdbId).subtitles.get(lang)) {
draft.movies.get(action.payload.main.imdbId).subtitles = draft.movies.get(imdbId).subtitles.get(lang).searching = true;
action.payload.response.data; }
break; break;
}
case "MOVIE_SUBTITLES_UPDATE_FULFILLED": {
let imdbId = action.payload.main.imdbId;
let lang = action.payload.main.lang;
let data = action.payload.response.data;
if (!draft.movies.get(imdbId)) {
break;
}
draft.movies.get(imdbId).fetchingSubtitles = draft.movies
.get(imdbId)
.fetchingSubtitles.filter((l) => l != lang);
if (data) {
draft.movies.get(imdbId).subtitles.set(lang, formatSubtitle(data));
}
break;
}
case "SELECT_MOVIE": case "SELECT_MOVIE":
draft.selectedImdbId = action.payload.imdbId; draft.selectedImdbId = action.payload.imdbId;

View File

@ -1,6 +1,6 @@
import { produce } from "immer"; import { produce } from "immer";
import { formatTorrents } from "../utils"; import { formatTorrents, formatSubtitle, formatSubtitles } from "./utils";
const defaultState = { const defaultState = {
loading: false, loading: false,
@ -10,10 +10,12 @@ const defaultState = {
const formatEpisode = (episode) => { const formatEpisode = (episode) => {
// Format the episode's torrents // Format the episode's torrents
episode.torrents = formatTorrents(episode); episode.torrents = formatTorrents(episode);
episode.subtitles = formatSubtitles(episode.subtitles);
// Set the default fetching data // Set the default fetching data
episode.fetching = false; episode.fetching = false;
episode.fetchingSubtitles = false; // Holds the languages of the subtitles currently fetching
episode.fetchingSubtitles = [];
}; };
export default (state = defaultState, action) => export default (state = defaultState, action) =>
@ -98,22 +100,42 @@ export default (state = defaultState, action) =>
break; break;
} }
case "EPISODE_SUBTITLES_UPDATE_PENDING": case "EPISODE_SUBTITLES_UPDATE_PENDING": {
let season = action.payload.main.season;
let episode = action.payload.main.episode;
let lang = action.payload.main.lang;
draft.show.seasons draft.show.seasons
.get(action.payload.main.season) .get(season)
.get(action.payload.main.episode).fetchingSubtitles = true; .get(episode)
break; .fetchingSubtitles.push(lang);
if (draft.show.seasons.get(season).get(episode).subtitles.get(lang)) {
case "EPISODE_SUBTITLES_UPDATE_FULFILLED": { draft.show.seasons
draft.show.seasons .get(season)
.get(action.payload.main.season) .get(episode)
.get(action.payload.main.episode).subtitles = .subtitles.get(lang).searching = true;
action.payload.response.data; }
draft.show.seasons
.get(action.payload.main.season)
.get(action.payload.main.episode).fetchingSubtitles = false;
break; break;
} }
case "EPISODE_SUBTITLES_UPDATE_FULFILLED": {
let season = action.payload.main.season;
let episode = action.payload.main.episode;
let lang = action.payload.main.lang;
let data = action.payload.response.data;
draft.show.seasons.get(season).get(episode).fetchingSubtitles =
draft.show.seasons
.get(season)
.get(episode)
.fetchingSubtitles.filter((l) => l != lang);
if (data) {
draft.show.seasons
.get(season)
.get(episode)
.subtitles.set(lang, formatSubtitle(data));
}
break;
}
default: default:
return draft; return draft;
} }

View File

@ -0,0 +1,51 @@
export const formatSubtitles = (subtitles) => {
if (!subtitles || subtitles.length == 0) {
return new Map();
}
let map = new Map();
subtitles.forEach((subtitle) => {
subtitle = formatSubtitle(subtitle);
map.set(subtitle.lang, subtitle);
});
return map;
};
export const formatSubtitle = (subtitle) => {
if (!subtitle) {
return undefined;
}
subtitle.searching = false;
return subtitle;
};
export const formatTorrents = (input) => {
if (!input.torrents || input.torrents.length == 0) {
return undefined;
}
let torrentMap = new Map();
input.torrents.forEach((torrent) => {
if (!torrent.result || !torrent.result.source) {
return;
}
if (!torrentMap.has(torrent.result.source)) {
torrentMap.set(torrent.result.source, new Map());
}
torrentMap.get(torrent.result.source).set(torrent.quality, torrent);
});
return torrentMap;
};
export const formatMovie = (movie) => {
movie.fetchingDetails = false;
movie.fetchingSubtitles = [];
movie.torrents = formatTorrents(movie);
movie.subtitles = formatSubtitles(movie.subtitles);
return movie;
};

View File

@ -35,26 +35,5 @@ export const prettySize = (fileSizeInBytes) => {
return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i]; return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i];
}; };
export const formatTorrents = (input) => {
if (!input.torrents || input.torrents.length == 0) {
return undefined;
}
let torrentMap = new Map();
input.torrents.forEach((torrent) => {
if (!torrent.result || !torrent.result.source) {
return;
}
if (!torrentMap.has(torrent.result.source)) {
torrentMap.set(torrent.result.source, new Map());
}
torrentMap.get(torrent.result.source).set(torrent.quality, torrent);
});
return torrentMap;
};
export const upperCaseFirst = (string) => export const upperCaseFirst = (string) =>
string.charAt(0).toUpperCase() + string.slice(1); string.charAt(0).toUpperCase() + string.slice(1);

18902
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,62 @@
{ {
"name": "canape", "name": "canape",
"scripts": { "scripts": {
"start": "NODE_ENV=development ./node_modules/webpack/bin/webpack.js -d --progress --colors --watch", "start": "NODE_ENV=development npx webpack --progress --color --watch",
"build": "NODE_ENV=production ./node_modules/webpack/bin/webpack.js -p --progress --colors", "build": "NODE_ENV=production npx webpack --progress --color",
"lint": "./node_modules/eslint/bin/eslint.js js" "lint": "npx eslint js"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^4.4.1", "bootstrap": "^4.4.1",
"bootswatch": "^4.4.1", "bootswatch": "^4.4.1",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"fuzzy": "^0.1.3", "fuzzy": "^0.1.3",
"history": "^4.9.0", "history": "^5.0.0",
"immer": "^6.0.3", "immer": "^9.0.5",
"jquery": "^3.4.1", "jquery": "^3.6.0",
"jwt-decode": "^2.1.0", "jwt-decode": "^3.1.2",
"moment": "^2.20.1", "moment": "^2.29.1",
"popper.js": "^1.15.0", "popper.js": "^1.15.0",
"prop-types": "^15.6.0", "prop-types": "^15.7.2",
"react": "^16.13.1", "react": "^17.0.2",
"react-bootstrap": "^1.0.0", "react-bootstrap": "^1.6.1",
"react-bootstrap-sweetalert": "^5.1.9", "react-bootstrap-sweetalert": "^5.2.0",
"react-bootstrap-toggle": "^2.2.6", "react-bootstrap-toggle": "^2.3.2",
"react-dom": "^16.13.1", "react-dom": "^17.0.2",
"react-infinite-scroll-component": "^5.0.4", "react-infinite-scroll-component": "^6.1.0",
"react-loading": "2.0.3", "react-loading": "2.0.3",
"react-redux": "^7.2.0", "react-redux": "^7.2.4",
"react-router-bootstrap": "^0.25.0", "react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.2.0",
"redux": "^4.0.5", "redux": "^4.1.1",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"ua-parser-js": "^0.7.17" "ua-parser-js": "^0.7.28"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.0", "@babel/core": "^7.15.0",
"@babel/preset-env": "^7.9.0", "@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.14.5",
"autoprefixer": "^9.7.5", "autoprefixer": "^10.3.1",
"axios": "^0.19.2", "axios": "^0.21.1",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.4.2", "css-loader": "^6.2.0",
"eslint": "^6.8.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^6.10.1", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.24.0",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.19.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^2.5.1", "eslint-plugin-react-hooks": "^4.2.0",
"file-loader": "^5.1.0", "html-webpack-plugin": "^5.3.2",
"html-webpack-plugin": "^3.2.0", "postcss-loader": "^6.1.1",
"node-sass": "^4.12.0", "prettier": "^2.3.2",
"postcss-loader": "^3.0.0", "sass": "^1.37.5",
"prettier": "^2.0.2", "sass-loader": "^12.1.0",
"sass-loader": "^8.0.2", "style-loader": "^3.2.1",
"style-loader": "^1.1.3", "universal-cookie": "^4.0.4",
"universal-cookie": "^4.0.3", "webpack": "^5.50.0",
"url-loader": "^3.0.0", "webpack-cli": "^4.7.2",
"webpack": "^4.42.1", "webpack-pwa-manifest": "^4.3.0"
"webpack-cli": "^3.3.11",
"webpack-pwa-manifest": "^4.0.0"
} }
} }

View File

@ -146,3 +146,8 @@ div.sweet-alert > h2 {
.toast { .toast {
background-color: $card-bg; background-color: $card-bg;
} }
.link-unstyled, .link-unstyled:link, .link-unstyled:hover {
color: inherit;
text-decoration: inherit;
}

View File

@ -20,19 +20,9 @@ const config = {
output: { output: {
path: BUILD_DIR, path: BUILD_DIR,
filename: "[contenthash]-app.js", filename: "[contenthash]-app.js",
assetModuleFilename: "[hash]-[name][ext][query]",
}, },
optimization: { optimization: {},
runtimeChunk: "single",
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
},
},
},
},
module: { module: {
rules: [ rules: [
{ {
@ -51,15 +41,15 @@ const config = {
}, },
{ {
test: /\.(png|jpg|svg|ico)$/, test: /\.(png|jpg|svg|ico)$/,
use: ["file-loader?name=[hash]-[name].[ext]"], type: "asset/resource",
}, },
{ {
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: ["url-loader?limit=10000&mimetype=application/font-woff"], type: "asset",
}, },
{ {
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: ["file-loader"], type: "asset",
}, },
], ],
}, },
@ -67,7 +57,6 @@ const config = {
new webpack.DefinePlugin({ new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}), }),
new webpack.HashedModuleIdsPlugin(),
new CleanWebpackPlugin({ new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ["**/*", "!img/**/*", "!img"], cleanOnceBeforeBuildPatterns: ["**/*", "!img/**/*", "!img"],
}), }),
@ -87,6 +76,7 @@ const config = {
theme_color: "#4e5d6c", // eslint-disable-line camelcase theme_color: "#4e5d6c", // eslint-disable-line camelcase
display: "standalone", display: "standalone",
orientation: "omit", orientation: "omit",
publicPath: "/",
scope: "/", scope: "/",
start_url: "/", // eslint-disable-line camelcase start_url: "/", // eslint-disable-line camelcase
icons: [ icons: [
@ -105,7 +95,7 @@ const config = {
resolve: { resolve: {
extensions: [".js"], extensions: [".js"],
}, },
devtool: mode === "production" ? "source-map" : "inline-source-map", devtool: mode === "production" ? false : "source-map",
}; };
module.exports = config; module.exports = config;