Compare commits

..

4 Commits

Author SHA1 Message Date
114f022b8d Fix empty torrents returned as array in show episodes
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
The show episodes can handle undefined values themselves.
2020-04-10 17:09:43 +02:00
83d1894a25 Fix torrent search on every keystroke
Cleanup the search results when leaving the page.
2020-04-10 17:09:43 +02:00
c9ecdac4f2 Add the torrent URL in the torrent button
This allows us to copy the link without clicking on the button.
2020-04-10 17:09:43 +02:00
3bd51765b8 Update papi and polochon to use the new torrent API 2020-04-10 17:09:43 +02:00
16 changed files with 184 additions and 188 deletions

View File

@ -42,40 +42,51 @@ type episodeTorrentDB struct {
Updated time.Time `db:"updated_at"`
}
// NewTorrentFromEpisodeTorrentDB returns a polochon.Torrent from an
// episodeTorrentDB
func NewTorrentFromEpisodeTorrentDB(eDB *episodeTorrentDB) *polochon.Torrent {
q, _ := polochon.StringToQuality(eDB.Quality)
// newTorrentFromEpisodeTorrentDB returns a polochon.Torrent from an
// EpisodeTorrentDB
func newTorrentFromEpisodeTorrentDB(eDB *episodeTorrentDB) *polochon.Torrent {
return &polochon.Torrent{
Quality: *q,
URL: eDB.URL,
Seeders: eDB.Seeders,
Leechers: eDB.Leechers,
Source: eDB.Source,
UploadUser: eDB.UploadUser,
Size: eDB.Size,
ImdbID: eDB.ImdbID,
Type: polochon.TypeEpisode,
Season: eDB.Season,
Episode: eDB.Episode,
Quality: polochon.Quality(eDB.Quality),
Result: &polochon.TorrentResult{
URL: eDB.URL,
Seeders: eDB.Seeders,
Leechers: eDB.Leechers,
Source: eDB.Source,
UploadUser: eDB.UploadUser,
Size: eDB.Size,
},
}
}
// NewEpisodeTorrentDB returns an episodeTorrentDB ready to be put in DB from a
// newEpisodeTorrentDB returns an episodeTorrentDB ready to be put in DB from a
// polochon.Torrent
func NewEpisodeTorrentDB(t *polochon.Torrent, imdbID string, season, episode int) *episodeTorrentDB {
return &episodeTorrentDB{
ImdbID: imdbID,
Season: season,
Episode: episode,
URL: t.URL,
Source: t.Source,
Quality: string(t.Quality),
UploadUser: t.UploadUser,
Seeders: t.Seeders,
Leechers: t.Leechers,
Size: t.Size,
func newEpisodeTorrentDB(t *polochon.Torrent) *episodeTorrentDB {
e := &episodeTorrentDB{
ImdbID: t.ImdbID,
Quality: string(t.Quality),
Season: t.Season,
Episode: t.Episode,
}
if t.Result == nil {
return e
}
e.URL = t.Result.URL
e.Source = t.Result.Source
e.UploadUser = t.Result.UploadUser
e.Seeders = t.Result.Seeders
e.Leechers = t.Result.Leechers
e.Size = t.Result.Size
return e
}
// GetEpisodeTorrents returns show episodes torrents from database
func GetEpisodeTorrents(db *sqlx.DB, imdbID string, season, episode int) ([]polochon.Torrent, error) {
func GetEpisodeTorrents(db *sqlx.DB, imdbID string, season, episode int) ([]*polochon.Torrent, error) {
var torrentsDB = []*episodeTorrentDB{}
err := db.Select(&torrentsDB, getEpisodeTorrentQuery, imdbID, season, episode)
if err != nil {
@ -86,19 +97,19 @@ func GetEpisodeTorrents(db *sqlx.DB, imdbID string, season, episode int) ([]polo
return nil, sql.ErrNoRows
}
var torrents []polochon.Torrent
var torrents []*polochon.Torrent
for _, torrentDB := range torrentsDB {
torrent := NewTorrentFromEpisodeTorrentDB(torrentDB)
torrents = append(torrents, *torrent)
torrent := newTorrentFromEpisodeTorrentDB(torrentDB)
torrents = append(torrents, torrent)
}
return torrents, nil
}
// UpsertEpisodeTorrent upserts an episode torrent from a polochon torrent
func UpsertEpisodeTorrent(db *sqlx.DB, torrent *polochon.Torrent, imdbID string, season, episode int) error {
func UpsertEpisodeTorrent(db *sqlx.DB, torrent *polochon.Torrent) error {
// Create the EpisodeTorrent ready to be put in db
eDB := NewEpisodeTorrentDB(torrent, imdbID, season, episode)
eDB := newEpisodeTorrentDB(torrent)
r, err := db.NamedQuery(upsertEpisodeTorrentQuery, eDB)
if err != nil {
return err

View File

@ -39,38 +39,47 @@ type movieTorrentDB struct {
Updated time.Time `db:"updated_at"`
}
// NewTorrentFromMovieTorrentDB creates a new polochon.Torrent from a
// newTorrentFromMovieTorrentDB creates a new polochon.Torrent from a
// movieTorrentDB
func NewTorrentFromMovieTorrentDB(mDB *movieTorrentDB) *polochon.Torrent {
q, _ := polochon.StringToQuality(mDB.Quality)
func newTorrentFromMovieTorrentDB(mDB *movieTorrentDB) *polochon.Torrent {
return &polochon.Torrent{
Quality: *q,
URL: mDB.URL,
Seeders: mDB.Seeders,
Leechers: mDB.Leechers,
Source: mDB.Source,
UploadUser: mDB.UploadUser,
Size: mDB.Size,
ImdbID: mDB.ImdbID,
Quality: polochon.Quality(mDB.Quality),
Type: polochon.TypeMovie,
Result: &polochon.TorrentResult{
URL: mDB.URL,
Seeders: mDB.Seeders,
Leechers: mDB.Leechers,
Source: mDB.Source,
UploadUser: mDB.UploadUser,
Size: mDB.Size,
},
}
}
// NewMovieTorrentDB returns a MovieTorrent ready to be put in DB from a
// newMovieTorrentDB returns a MovieTorrent ready to be put in DB from a
// Torrent
func NewMovieTorrentDB(t *polochon.Torrent, imdbID string) movieTorrentDB {
return movieTorrentDB{
ImdbID: imdbID,
URL: t.URL,
Source: t.Source,
Quality: string(t.Quality),
UploadUser: t.UploadUser,
Seeders: t.Seeders,
Leechers: t.Leechers,
Size: t.Size,
func newMovieTorrentDB(t *polochon.Torrent) movieTorrentDB {
m := movieTorrentDB{
ImdbID: t.ImdbID,
Quality: string(t.Quality),
}
if t.Result == nil {
return m
}
m.URL = t.Result.URL
m.Source = t.Result.Source
m.UploadUser = t.Result.UploadUser
m.Seeders = t.Result.Seeders
m.Leechers = t.Result.Leechers
m.Size = t.Result.Size
return m
}
// GetMovieTorrents returns polochon.Torrents from the database
func GetMovieTorrents(db *sqlx.DB, imdbID string) ([]polochon.Torrent, error) {
func GetMovieTorrents(db *sqlx.DB, imdbID string) ([]*polochon.Torrent, error) {
var torrentsDB = []*movieTorrentDB{}
// Get the torrents from the DB
err := db.Select(&torrentsDB, getMovieTorrentQueryByImdbID, imdbID)
@ -83,18 +92,18 @@ func GetMovieTorrents(db *sqlx.DB, imdbID string) ([]polochon.Torrent, error) {
}
// Create polochon Torrents from the movieTorrentDB
var torrents []polochon.Torrent
var torrents []*polochon.Torrent
for _, torrentDB := range torrentsDB {
torrent := NewTorrentFromMovieTorrentDB(torrentDB)
torrents = append(torrents, *torrent)
torrent := newTorrentFromMovieTorrentDB(torrentDB)
torrents = append(torrents, torrent)
}
return torrents, nil
}
// UpsertMovieTorrent adds or updates MovieTorrent in db
func UpsertMovieTorrent(db *sqlx.DB, t *polochon.Torrent, imdbID string) error {
mDB := NewMovieTorrentDB(t, imdbID)
func UpsertMovieTorrent(db *sqlx.DB, t *polochon.Torrent) error {
mDB := newMovieTorrentDB(t)
r, err := db.NamedQuery(upsertMovieTorrentQuery, mDB)
if err != nil {
return err

View File

@ -223,7 +223,7 @@ func (m *Movie) RefreshTorrents(env *web.Env, torrenters []polochon.Torrenter) e
// Update them in database
for _, t := range m.Movie.Torrents {
err = models.UpsertMovieTorrent(env.Database, &t, m.ImdbID)
err = models.UpsertMovieTorrent(env.Database, t)
if err != nil {
log.Error("error while adding torrent", err)
continue

View File

@ -179,7 +179,7 @@ func RefreshTorrents(env *web.Env, showEpisode *polochon.ShowEpisode, torrenters
// Upsert all the torrents we got
for _, t := range showEpisode.Torrents {
err = models.UpsertEpisodeTorrent(env.Database, &t, showEpisode.ShowImdbID, showEpisode.Season, showEpisode.Episode)
err = models.UpsertEpisodeTorrent(env.Database, t)
if err != nil {
log.Errorf("error while adding torrent : %s", err)
continue

View File

@ -16,19 +16,12 @@ import (
// DownloadHandler downloads a movie via polochon
func DownloadHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
var data struct {
URL string `json:"url"`
Metadata *papi.TorrentMetadata `json:"metadata"`
}
err := json.NewDecoder(r.Body).Decode(&data)
torrent := &papi.Torrent{}
err := json.NewDecoder(r.Body).Decode(torrent)
if err != nil {
return env.RenderError(w, errors.New("failed to get the url"))
}
if data.URL == "" {
return env.RenderError(w, errors.New("no given url"))
}
user := auth.GetCurrentUser(r, env.Log)
client, err := user.NewPapiClient(env.Database)
@ -36,7 +29,7 @@ func DownloadHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error
return env.RenderError(w, err)
}
err = client.AddTorrent(data.URL, data.Metadata)
err = client.AddTorrent(torrent)
if err != nil {
return env.RenderError(w, err)
}
@ -123,7 +116,15 @@ func SearchHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
// Sort by seeds
sort.Slice(results, func(i, j int) bool {
return results[i].Seeders > results[j].Seeders
// Let's avoid the panic while sorting
if results[i].Result == nil {
results[i].Result = &polochon.TorrentResult{}
}
if results[j].Result == nil {
results[j].Result = &polochon.TorrentResult{}
}
return results[i].Result.Seeders > results[j].Result.Seeders
})
return env.RenderJSON(w, results)

View File

@ -3,14 +3,9 @@ import { configureAxios, request } from "../requests";
import { addAlertOk } from "./alerts";
export function addTorrent(torrent) {
return request(
"ADD_TORRENT",
configureAxios().post("/torrents", {
url: torrent.url,
metadata: torrent.metadata,
}),
[addAlertOk("Torrent added")]
);
return request("ADD_TORRENT", configureAxios().post("/torrents", torrent), [
addAlertOk("Torrent added"),
]);
}
export function removeTorrent(id) {
@ -27,6 +22,12 @@ export function searchTorrents(url) {
return request("TORRENTS_SEARCH", configureAxios().get(url));
}
export function clearTorrentSearch() {
return {
type: "TORRENTS_SEARCH_CLEAR",
};
}
export function setFetchedTorrents(torrents) {
return {
type: "TORRENTS_FETCH_FULFILLED",

View File

@ -27,7 +27,7 @@ const buildMenuItems = (torrents) => {
entries.push({
type: "entry",
quality: torrent.quality,
size: torrent.size,
size: torrent.result.size,
torrent: torrent,
});
});
@ -109,7 +109,11 @@ export const TorrentsButton = ({ torrents, search, searching, url }) => {
return (
<Dropdown.Item
key={index}
onClick={() => dispatch(addTorrent(e.torrent))}
href={e.torrent.result.url}
onClick={(event) => {
event.preventDefault();
dispatch(addTorrent(e.torrent));
}}
>
{e.quality}
{e.size !== 0 && (

View File

@ -19,10 +19,8 @@ export const EpisodeTorrentsButton = ({ season, episode }) => {
? state.show.show.seasons.get(season).get(episode).fetching
: false
);
const torrents = useSelector((state) =>
state.show.show.seasons.get(season).get(episode).torrents
? state.show.show.seasons.get(season).get(episode).torrents
: []
const torrents = useSelector(
(state) => state.show.show.seasons.get(season).get(episode).torrents
);
const url = useSelector(

View File

@ -25,7 +25,11 @@ const AddTorrent = () => {
if (url === "") {
return;
}
dispatch(addTorrent({ url: url, metdata: null }));
dispatch(
addTorrent({
result: { url: url },
})
);
setUrl("");
};
@ -66,28 +70,28 @@ const Torrents = () => {
const Torrent = ({ torrent }) => {
const dispatch = useDispatch();
var progressStyle = torrent.is_finished
var progressStyle = torrent.status.is_finished
? "success"
: "info progress-bar-striped progress-bar-animated";
const progressBarClass = "progress-bar bg-" + progressStyle;
var percentDone = torrent.percent_done;
var percentDone = torrent.status.percent_done;
const started = percentDone !== 0;
if (started) {
percentDone = Number(percentDone).toFixed(1) + "%";
}
// Pretty sizes
const downloadedSize = prettySize(torrent.downloaded_size);
const totalSize = prettySize(torrent.total_size);
const downloadRate = prettySize(torrent.download_rate) + "/s";
const downloadedSize = prettySize(torrent.status.downloaded_size);
const totalSize = prettySize(torrent.status.total_size);
const downloadRate = prettySize(torrent.status.download_rate) + "/s";
return (
<div className="card w-100 mb-3">
<h5 className="card-header">
<span className="text text-break">{torrent.name}</span>
<span className="text text-break">{torrent.status.name}</span>
<span
className="fa fa-trash clickable pull-right"
onClick={() => dispatch(removeTorrent(torrent.id))}
onClick={() => dispatch(removeTorrent(torrent.status.id))}
></span>
</h5>
<div className="card-body pb-0">

View File

@ -1,40 +1,39 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { useDispatch, useSelector } from "react-redux";
import { addTorrent, searchTorrents } from "../../actions/torrents";
import Loader from "../loader/loader";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { prettySize } from "../../utils";
import {
addTorrent,
searchTorrents,
clearTorrentSearch,
} from "../../actions/torrents";
export const TorrentSearch = ({ history, match }) => {
const dispatch = useDispatch();
const [search, setSearch] = useState(match.params.search || "");
const [type, setType] = useState(match.params.type || "");
const url = useCallback(() => {
const searchFunc = (type = "") => {
if (search === "" || type === "") {
return "";
}
return `/torrents/search/${type}/${encodeURI(search)}`;
}, [type, search]);
const searchFunc = useCallback(() => {
const searchURL = url();
if (searchURL === "") {
return;
}
dispatch(searchTorrents(searchURL));
history.push(searchURL);
}, [dispatch, history, url]);
const url = `/torrents/search/${type}/${encodeURI(search)}`;
setType(type);
dispatch(searchTorrents(url));
history.push(url);
};
useEffect(() => {
searchFunc();
}, [searchFunc]);
searchFunc(type);
return () => dispatch(clearTorrentSearch());
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="row">
@ -52,8 +51,7 @@ export const TorrentSearch = ({ history, match }) => {
type="movies"
typeFromURL={type}
handleClick={() => {
setType("movies");
searchFunc();
searchFunc("movies");
}}
/>
<SearchButton
@ -61,14 +59,13 @@ export const TorrentSearch = ({ history, match }) => {
type="shows"
typeFromURL={type}
handleClick={() => {
setType("shows");
searchFunc();
searchFunc("shows");
}}
/>
</div>
</div>
<div className="col-12">
<TorrentList search={search} />
<TorrentList />
</div>
</div>
);
@ -98,7 +95,7 @@ SearchButton.propTypes = {
handleClick: PropTypes.func.isRequired,
};
const TorrentList = ({ search }) => {
const TorrentList = () => {
const searching = useSelector((state) => state.torrents.searching);
const results = useSelector((state) => state.torrents.searchResults);
@ -106,10 +103,6 @@ const TorrentList = ({ search }) => {
return <Loader />;
}
if (search === "") {
return null;
}
if (results.size === 0) {
return (
<div className="jumbotron">
@ -126,9 +119,6 @@ const TorrentList = ({ search }) => {
</React.Fragment>
);
};
TorrentList.propTypes = {
search: PropTypes.string,
};
const Torrent = ({ torrent }) => {
const dispatch = useDispatch();
@ -136,17 +126,17 @@ const Torrent = ({ torrent }) => {
return (
<div className="alert d-flex border-bottom border-secondary align-items-center">
<TorrentHealth
url={torrent.url}
seeders={torrent.seeders}
leechers={torrent.leechers}
url={torrent.result.url}
seeders={torrent.result.seeders}
leechers={torrent.result.leechers}
/>
<span className="mx-3 text text-start text-break flex-fill">
{torrent.name}
{torrent.result.name}
</span>
<div>
{torrent.size !== 0 && (
{torrent.result.size !== 0 && (
<span className="mx-1 badge badge-pill badge-warning">
{prettySize(torrent.size)}
{prettySize(torrent.result.size)}
</span>
)}
@ -154,18 +144,16 @@ const Torrent = ({ torrent }) => {
{torrent.quality}
</span>
<span className="mx-1 badge badge-pill badge-success">
{torrent.source}
{torrent.result.source}
</span>
<span className="mx-1 badge badge-pill badge-info">
{torrent.upload_user}
{torrent.result.upload_user}
</span>
</div>
<div className="align-self-end ml-3">
<i
className="fa fa-cloud-download clickable"
onClick={() =>
dispatch(addTorrent({ url: torrent.url, metadata: null }))
}
onClick={() => dispatch(addTorrent(torrent))}
></i>
</div>
</div>

View File

@ -1,5 +1,7 @@
import { produce } from "immer";
import { formatTorrents } from "../utils";
const defaultState = {
loading: false,
movies: new Map(),
@ -8,29 +10,6 @@ const defaultState = {
exploreOptions: {},
};
const formatTorrents = (movie) => {
if (!movie.torrents || movie.torrents.length == 0) {
return undefined;
}
let torrentMap = new Map();
movie.torrents.forEach((torrent) => {
if (!torrentMap.has(torrent.source)) {
torrentMap.set(torrent.source, new Map());
}
torrent.metadata = {
type: "movie",
imdb_id: movie.imdb_id, // eslint-disable-line camelcase
quality: torrent.quality,
};
torrentMap.get(torrent.source).set(torrent.quality, torrent);
});
return torrentMap;
};
const formatMovie = (movie) => {
movie.fetchingDetails = false;
movie.fetchingSubtitles = false;

View File

@ -1,35 +1,12 @@
import { produce } from "immer";
import { formatTorrents } from "../utils";
const defaultState = {
loading: false,
show: {},
};
const formatTorrents = (episode) => {
if (!episode.torrents || episode.torrents.length == 0) {
return undefined;
}
let torrentMap = new Map();
episode.torrents.forEach((torrent) => {
if (!torrentMap.has(torrent.source)) {
torrentMap.set(torrent.source, new Map());
}
torrent.metadata = {
type: "episode",
imdb_id: episode.show_imdb_id, // eslint-disable-line camelcase
quality: torrent.quality,
season: episode.season,
episode: episode.episode,
};
torrentMap.get(torrent.source).set(torrent.quality, torrent);
});
return torrentMap;
};
const formatEpisode = (episode) => {
// Format the episode's torrents
episode.torrents = formatTorrents(episode);

View File

@ -28,6 +28,10 @@ export default (state = defaultState, action) =>
draft.searchResults = action.payload.response.data;
break;
case "TORRENTS_SEARCH_CLEAR":
draft.searchResults = [];
break;
default:
return draft;
}

View File

@ -31,3 +31,24 @@ export const prettySize = (fileSizeInBytes) => {
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;
};

4
go.mod
View File

@ -15,8 +15,8 @@ require (
github.com/mattn/go-sqlite3 v1.10.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/odwrtw/errors v0.0.0-20170604160533-c747b9d17833
github.com/odwrtw/papi v0.0.0-20200408160729-930e92b452fd
github.com/odwrtw/polochon v0.0.0-20200408160701-0455bb96acb0
github.com/odwrtw/papi v0.0.0-20200410143325-49e6f827259d
github.com/odwrtw/polochon v0.0.0-20200410143337-006e3fb9fb55
github.com/phyber/negroni-gzip v0.0.0-20180113114010-ef6356a5d029
github.com/pioz/tvdb v0.0.0-20190503215423-f45c687faba9 // indirect
github.com/robfig/cron v1.1.0

9
go.sum
View File

@ -133,12 +133,11 @@ github.com/odwrtw/guessit v0.0.0-20200131084001-f88613483547 h1:O0wEl/ORBHpPpZti
github.com/odwrtw/guessit v0.0.0-20200131084001-f88613483547/go.mod h1:W22g7wtc0AGczEARBAs+77gvBk8monDaM6U6i9Wa0vQ=
github.com/odwrtw/imdb-watchlist v0.0.0-20190417175016-b7a9f7503d69 h1:ow6b/4Jj7J5iYwU678/rbijvaNUJrYkg13j9Nivkung=
github.com/odwrtw/imdb-watchlist v0.0.0-20190417175016-b7a9f7503d69/go.mod h1:o2tLH95CtNdqhDb0aS2NbU+1I4PmaNsODpr33Ry0JC0=
github.com/odwrtw/papi v0.0.0-20190413103029-bd5bfea85ae6 h1:bF8XKFfYNY4quRdqJ5E9ERd+FdR26H1X1Z2fNRGePSk=
github.com/odwrtw/papi v0.0.0-20190413103029-bd5bfea85ae6/go.mod h1:CXotdtODLpW0/yuFV5XH8Rmrj0eAfPLvdMKykPM2WCk=
github.com/odwrtw/papi v0.0.0-20200408160729-930e92b452fd h1:LsBK0gVXC8oRxyAwvkCg5fQWTD678Jl7n6a8MZpdUKo=
github.com/odwrtw/papi v0.0.0-20200408160729-930e92b452fd/go.mod h1:eY0skvVHJBwbSJ18uq2c1T4SvhdEV8R0XFSb0zKh5Yo=
github.com/odwrtw/polochon v0.0.0-20200408160701-0455bb96acb0 h1:p0pXoG89JVL/bZWJpqu2KRQwCU44yYnNnoP46SIPNPQ=
github.com/odwrtw/polochon v0.0.0-20200408160701-0455bb96acb0/go.mod h1:sAYf/A5tDmins2GHZn2mEFarmYltAZv+bcmSKSxDUaI=
github.com/odwrtw/papi v0.0.0-20200410143325-49e6f827259d h1:it4hnCveS8eFymg0ll9KRzO/iQm/olSW0sb8Ctm3gXI=
github.com/odwrtw/papi v0.0.0-20200410143325-49e6f827259d/go.mod h1:eY0skvVHJBwbSJ18uq2c1T4SvhdEV8R0XFSb0zKh5Yo=
github.com/odwrtw/polochon v0.0.0-20200410143337-006e3fb9fb55 h1:hcHBTi+HfYz5p6wgtvQCbrdog0uOB/7eVxPZA5Qff80=
github.com/odwrtw/polochon v0.0.0-20200410143337-006e3fb9fb55/go.mod h1:sAYf/A5tDmins2GHZn2mEFarmYltAZv+bcmSKSxDUaI=
github.com/odwrtw/tpb v0.0.0-20200130133144-c846aa382c6f h1:fwEIGT+o3e8+XkBqrwsE3/+9ketTQXflPhCkv3/w990=
github.com/odwrtw/tpb v0.0.0-20200130133144-c846aa382c6f/go.mod h1:updLvMbQo2xHoz94MX9+GqmSoKhf6E8fs/J+wLvvu6A=
github.com/odwrtw/trakttv v0.0.0-20200404161731-0d594827e4f9 h1:PuQLHO75MXUsJpf9BcTVxvR/FCkdn1MZnZt6h3o6cJI=