Version with global json

This commit is contained in:
Thomas Forgione 2020-10-05 11:26:11 +02:00
parent 00b68959e5
commit 98c34600d1
7 changed files with 130 additions and 315 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ js/main.js
js/main.tmp.js js/main.tmp.js
js/main.min.js js/main.min.js
deploy.sh deploy.sh
index.json

View File

@ -37,6 +37,12 @@
}); });
}); });
} }
if (app.ports !== undefined && app.ports.eraseVideo !== undefined) {
app.ports.eraseVideo.subscribe(function() {
lastId = undefined;
});
}
</script> </script>
</body> </body>
</html> </html>

28
indexify.js Normal file
View File

@ -0,0 +1,28 @@
const fs = require('fs');
const path = require('path');
const VIDEO_DIR = "videos";
const DESCRIPTION_FILE = "description.json";
let info = [];
for (let dir of fs.readdirSync(VIDEO_DIR)) {
let description = JSON.parse(fs.readFileSync(path.join(VIDEO_DIR, dir, DESCRIPTION_FILE)));
description.url = dir + "/";
description.videos = [];
for (let subdir of fs.readdirSync(path.join(VIDEO_DIR, dir))) {
if (subdir === DESCRIPTION_FILE) {
continue;
}
let subdescription = JSON.parse(fs.readFileSync(path.join(VIDEO_DIR, dir, subdir, DESCRIPTION_FILE)));
subdescription.url = subdir + "/";
description.videos.push(subdescription);
}
info.push(description);
}
fs.writeFileSync('index.json', JSON.stringify(info));

View File

@ -4,8 +4,8 @@ import Browser.Events as Events
import Browser.Navigation as Nav import Browser.Navigation as Nav
import Dict exposing (Dict) import Dict exposing (Dict)
import Element import Element
import Http
import Ports import Ports
import Task
import Time import Time
import Twitch import Twitch
import Url import Url
@ -27,19 +27,16 @@ type alias Model =
type Page type Page
= Home = Home
| Loading | Playlist Twitch.Playlist
| Playlist Twitch.PlaylistWithVideos | Video Twitch.Playlist Twitch.Video
| Video Twitch.PlaylistWithVideos Twitch.Video
type Msg type Msg
= Noop = Noop
| PlaylistsReceived ( List Twitch.Playlist, Time.Zone ) | PlaylistsReceived ( List Twitch.Playlist, Time.Zone )
| HomeClicked | HomeClicked
| PlaylistClicked String | PlaylistClicked Twitch.Playlist
| PlaylistReceived Twitch.PlaylistWithVideos | VideoClicked Twitch.Playlist Twitch.Video
| PlaylistReceivedVideo String (Maybe Int) Twitch.PlaylistWithVideos
| VideoClicked Twitch.PlaylistWithVideos Twitch.Video
| UrlReceived Url.Url | UrlReceived Url.Url
| SizeReceived Int Int | SizeReceived Int Int
@ -47,10 +44,20 @@ type Msg
init : { width : Int, height : Int } -> Url.Url -> Nav.Key -> ( FullModel, Cmd Msg ) init : { width : Int, height : Int } -> Url.Url -> Nav.Key -> ( FullModel, Cmd Msg )
init { width, height } url key = init { width, height } url key =
( Unloaded (Element.classifyDevice { width = width, height = height }) url key ( Unloaded (Element.classifyDevice { width = width, height = height }) url key
, Task.perform PlaylistsReceived Twitch.fetchPlaylists , Twitch.fetchPlaylists resultToMsg
) )
resultToMsg : Result Http.Error (List Twitch.Playlist) -> Msg
resultToMsg result =
case result of
Ok o ->
PlaylistsReceived ( o, Time.utc )
_ ->
Noop
subscriptions : FullModel -> Sub Msg subscriptions : FullModel -> Sub Msg
subscriptions _ = subscriptions _ =
Events.onResize (\w h -> SizeReceived w h) Events.onResize (\w h -> SizeReceived w h)
@ -79,12 +86,12 @@ update msg model =
( PlaylistClicked playlist, Loaded m ) -> ( PlaylistClicked playlist, Loaded m ) ->
( model ( model
, Nav.pushUrl m.key ("#" ++ playlist) , Nav.pushUrl m.key ("#" ++ playlist.url)
) )
( VideoClicked playlist video, Loaded m ) -> ( VideoClicked playlist video, Loaded m ) ->
( model ( model
, Nav.pushUrl m.key ("#" ++ playlist.url ++ Twitch.videoName video) , Nav.pushUrl m.key ("#" ++ playlist.url ++ video.url)
) )
( UrlReceived url, Loaded m ) -> ( UrlReceived url, Loaded m ) ->
@ -95,14 +102,10 @@ update msg model =
( split, args ) = ( split, args ) =
case splits of case splits of
h1 :: h2 :: _ -> h1 :: h2 :: _ ->
( String.split "/" h1 |> List.filter (not << String.isEmpty) ( String.split "/" h1, parseQueryString h2 )
, parseQueryString h2
)
h1 :: _ -> h1 :: _ ->
( String.split "/" h1 |> List.filter (not << String.isEmpty) ( String.split "/" h1, Dict.empty )
, Dict.empty
)
_ -> _ ->
( [], Dict.empty ) ( [], Dict.empty )
@ -130,72 +133,38 @@ update msg model =
( Nothing, Nothing ) ( Nothing, Nothing )
playlist = playlist =
case m.page of List.head (List.filter (\x -> Just x.url == playlistName) m.playlists)
Playlist p ->
Just p
Video p _ -> video =
Just p
_ ->
Nothing
realVideo =
case playlist of case playlist of
Just p -> Just p ->
case List.head (List.filter (\x -> Just (Twitch.videoName x) == videoName) p.videos) of List.head (List.filter (\x -> Just x.url == videoName) p.videos)
Just v ->
Just v
_ ->
Nothing
_ -> _ ->
Nothing Nothing
( page, cmd ) = ( page, cmd ) =
case ( ( playlist, realVideo ), ( playlistName, videoName ) ) of case ( playlist, video ) of
( ( Just p, _ ), ( Just _, Nothing ) ) -> ( Just p, Just v ) ->
( Playlist p ( Video p v
, Cmd.none , Ports.registerVideo ( Twitch.videoId v, "videos/" ++ p.url ++ v.url, time )
) )
( ( Just p, Just video ), ( Just _, Just _ ) ) -> ( Just p, Nothing ) ->
( Video p video ( Playlist p, Cmd.none )
, Ports.registerVideo ( Twitch.videoId video, video.url, time )
)
( ( _, _ ), ( Just name, Nothing ) ) ->
( Loading
, Task.perform
PlaylistReceived
(Twitch.fetchPlaylistWithVideos name)
)
( ( _, _ ), ( Just name, Just video ) ) ->
( Loading
, Task.perform
(PlaylistReceivedVideo video time)
(Twitch.fetchPlaylistWithVideos name)
)
_ -> _ ->
( Home, Cmd.none ) ( Home, Cmd.none )
extraCmd =
case page of
Video _ _ ->
Cmd.none
_ ->
Ports.eraseVideo ()
in in
( Loaded { m | page = page }, cmd ) ( Loaded { m | page = page }, Cmd.batch [ cmd, extraCmd ] )
( PlaylistReceived p, Loaded m ) ->
( Loaded { m | page = Playlist p }, Cmd.none )
( PlaylistReceivedVideo video time playlist, Loaded m ) ->
case List.head (List.filter (\x -> Twitch.videoName x == video) playlist.videos) of
Just v ->
( Loaded { m | page = Video playlist v }
, Ports.registerVideo ( Twitch.videoId v, v.url, time )
)
_ ->
( Loaded { m | page = Home }, Cmd.none )
_ -> _ ->
( model, Cmd.none ) ( model, Cmd.none )

View File

@ -1,4 +1,7 @@
port module Ports exposing (registerVideo) port module Ports exposing (eraseVideo, registerVideo)
port registerVideo : ( String, String, Maybe Int ) -> Cmd msg port registerVideo : ( String, String, Maybe Int ) -> Cmd msg
port eraseVideo : () -> Cmd msg

View File

@ -1,8 +1,7 @@
module Twitch exposing module Twitch exposing
( Playlist ( Playlist
, PlaylistWithVideos
, Video , Video
, fetchPlaylistWithVideos , decodePlaylists
, fetchPlaylists , fetchPlaylists
, playlistMiniatureUrl , playlistMiniatureUrl
, videoId , videoId
@ -10,28 +9,12 @@ module Twitch exposing
, videoName , videoName
) )
import Html.Parser
import Http import Http
import Iso8601 import Iso8601
import Json.Decode as Decode import Json.Decode as Decode
import Task exposing (Task)
import Time import Time
type alias Playlist =
{ url : String
, name : String
, videos : List String
}
type alias PlaylistWithVideos =
{ url : String
, name : String
, videos : List Video
}
type alias Video = type alias Video =
{ name : String { name : String
, url : String , url : String
@ -40,9 +23,46 @@ type alias Video =
} }
type alias Playlist =
{ url : String
, name : String
, videos : List Video
}
decodeVideo : Decode.Decoder Video
decodeVideo =
Decode.map4 Video
(Decode.field "title" Decode.string)
(Decode.field "url" Decode.string)
(Decode.field "duration" Decode.int)
(Decode.maybe (Decode.field "date" Iso8601.decoder))
decodePlaylist : Decode.Decoder Playlist
decodePlaylist =
Decode.map3 Playlist
(Decode.field "url" Decode.string)
(Decode.field "title" Decode.string)
(Decode.field "videos" (Decode.map (List.sortBy .url >> List.reverse) (Decode.list decodeVideo)))
decodePlaylists : Decode.Decoder (List Playlist)
decodePlaylists =
Decode.map (List.sortBy .url >> List.reverse) (Decode.list decodePlaylist)
fetchPlaylists : (Result Http.Error (List Playlist) -> msg) -> Cmd msg
fetchPlaylists resultToMsg =
Http.get
{ url = "/index.json"
, expect = Http.expectJson resultToMsg decodePlaylists
}
videoName : Video -> String videoName : Video -> String
videoName video = videoName video =
String.join "/" (List.drop 3 (String.split "/" video.url)) video.url
videoId : Video -> String videoId : Video -> String
@ -50,222 +70,16 @@ videoId video =
String.dropLeft 1 video.url |> String.replace "/" "-" String.dropLeft 1 video.url |> String.replace "/" "-"
get : { url : String, resolver : Http.Resolver x a } -> Task x a
get { url, resolver } =
Http.task
{ body = Http.emptyBody
, headers = []
, method = "GET"
, resolver = resolver
, timeout = Nothing
, url = url
}
playlistMiniatureUrl : Playlist -> String playlistMiniatureUrl : Playlist -> String
playlistMiniatureUrl playlist = playlistMiniatureUrl playlist =
case List.head (List.reverse playlist.videos) of case List.head (List.reverse playlist.videos) of
Just v -> Just v ->
"videos/" ++ playlist.url ++ v ++ "miniature-050.png" "videos/" ++ playlist.url ++ v.url ++ "miniature-050.png"
_ -> _ ->
"" ""
videoMiniatureUrl : Video -> String videoMiniatureUrl : Playlist -> Video -> String
videoMiniatureUrl video = videoMiniatureUrl playlist video =
video.url ++ "miniature-050.png" "videos/" ++ playlist.url ++ video.url ++ "miniature-050.png"
sortPlaylistWithVideos : PlaylistWithVideos -> PlaylistWithVideos
sortPlaylistWithVideos playlist =
{ playlist | videos = List.sortBy .url playlist.videos |> List.reverse }
sortPlaylists : List Playlist -> List Playlist
sortPlaylists playlists =
List.sortBy .url playlists |> List.reverse
fetchPlaylists : Task x ( List Playlist, Time.Zone )
fetchPlaylists =
fetchPlaylistPath
|> Task.andThen fetchPlaylistsMapper
|> Task.map sortPlaylists
|> Task.andThen fetchTimezone
fetchTimezone : List Playlist -> Task x ( List Playlist, Time.Zone )
fetchTimezone playlists =
Task.map (\zone -> ( playlists, zone )) Time.here
fetchPlaylistPath : Task x (List String)
fetchPlaylistPath =
get
{ url = "/videos"
, resolver = Http.stringResolver parseHrefs
}
fetchPlaylist : String -> Task x Playlist
fetchPlaylist name =
fetchPlaylistName name
|> Task.andThen
(\a ->
fetchPlaylistVideoPaths name
-- |> Task.andThen (\c -> fetchVideos name c)
|> Task.map (Playlist name a)
)
fetchPlaylistWithVideos : String -> Task x PlaylistWithVideos
fetchPlaylistWithVideos name =
fetchPlaylistName name
|> Task.andThen
(\a ->
fetchPlaylistVideoPaths name
|> Task.andThen (\c -> fetchVideos name c)
|> Task.map (PlaylistWithVideos name a)
|> Task.map sortPlaylistWithVideos
)
fetchVideo : String -> String -> Task x Video
fetchVideo playlist video =
let
url =
"/videos/" ++ playlist ++ video
in
get
{ url = url ++ "description.json"
, resolver = Http.stringResolver (parseVideo url)
}
fetchVideos : String -> List String -> Task x (List Video)
fetchVideos playlist videos =
Task.sequence (List.map (fetchVideo playlist) videos)
fetchPlaylistName : String -> Task x String
fetchPlaylistName path =
get
{ url = "/videos/" ++ path ++ "description.json"
, resolver = Http.stringResolver parsePlaylistName
}
fetchPlaylistVideoPaths : String -> Task x (List String)
fetchPlaylistVideoPaths name =
get
{ url = "/videos/" ++ name
, resolver = Http.stringResolver parseHrefs
}
parseVideo : String -> Http.Response String -> Result x Video
parseVideo url result =
case result of
Http.GoodStatus_ _ content ->
case Decode.decodeString (decodeVideo url) content of
Ok v ->
Ok v
_ ->
Ok { name = "", url = url, duration = 0, date = Nothing }
_ ->
Ok { name = "", url = url, duration = 0, date = Nothing }
fetchPlaylistsMapper : List String -> Task x (List Playlist)
fetchPlaylistsMapper names =
Task.sequence (List.map fetchPlaylist names)
parsePlaylistName : Http.Response String -> Result x String
parsePlaylistName result =
case result of
Http.GoodStatus_ _ content ->
case Decode.decodeString decodePlaylistName content of
Ok p ->
Ok p
_ ->
Ok ""
_ ->
Ok ""
parseHrefs : Http.Response String -> Result x (List String)
parseHrefs result =
case result of
Http.GoodStatus_ _ content ->
let
withoutDoctype =
if String.startsWith "<!doctype" (String.toLower content) then
String.lines content |> List.drop 1 |> String.join "\n"
else
content
decoded =
Html.Parser.run withoutDoctype
hrefs =
Result.map findHrefs decoded
in
case hrefs of
Ok h ->
Ok h
_ ->
Ok []
_ ->
Ok []
findHrefsAux : List String -> Html.Parser.Node -> List String
findHrefsAux acc node =
case node of
Html.Parser.Element string (( key, value ) :: t) nodes ->
let
newAcc =
if key == "href" && String.endsWith "/" value && value /= "../" then
value :: acc
else
acc
in
findHrefsAux newAcc (Html.Parser.Element string t nodes)
Html.Parser.Element string [] (h :: t) ->
let
attrs =
findHrefsAux [] h
in
findHrefsAux (acc ++ attrs) (Html.Parser.Element string [] t)
_ ->
acc
findHrefs : List Html.Parser.Node -> List String
findHrefs x =
findHrefsAux [] (Html.Parser.Element "" [] x)
decodePlaylistName : Decode.Decoder String
decodePlaylistName =
Decode.field "title" Decode.string
decodeVideo : String -> Decode.Decoder Video
decodeVideo url =
Decode.map3 (\x y -> Video x url y)
(Decode.field "title" Decode.string)
(Decode.map Basics.round (Decode.field "duration" Decode.float))
(Decode.maybe (Decode.field "date" Iso8601.decoder))

View File

@ -55,9 +55,6 @@ title model =
Core.Home -> Core.Home ->
Consts.url Consts.url
Core.Loading ->
Consts.url
Core.Playlist p -> Core.Playlist p ->
Consts.url ++ " - " ++ p.name Consts.url ++ " - " ++ p.name
@ -71,9 +68,6 @@ viewContent model =
Core.Home -> Core.Home ->
playlistsView model.device model.playlists playlistsView model.device model.playlists
Core.Loading ->
Element.el [ Element.padding 10, Element.centerX ] spinner
Core.Playlist playlist -> Core.Playlist playlist ->
videoMiniaturesView model.device model.zone playlist videoMiniaturesView model.device model.zone playlist
@ -177,13 +171,13 @@ playlistView playlist =
button = button =
Input.button [ Element.width Element.fill, Element.alignTop ] Input.button [ Element.width Element.fill, Element.alignTop ]
{ label = display { label = display
, onPress = Just (Core.PlaylistClicked playlist.url) , onPress = Just (Core.PlaylistClicked playlist)
} }
in in
button button
videoMiniaturesView : Element.Device -> Time.Zone -> Twitch.PlaylistWithVideos -> Element Core.Msg videoMiniaturesView : Element.Device -> Time.Zone -> Twitch.Playlist -> Element Core.Msg
videoMiniaturesView device zone playlist = videoMiniaturesView device zone playlist =
let let
empty = empty =
@ -206,8 +200,8 @@ videoMiniaturesView device zone playlist =
final final
videoMiniature : Twitch.Video -> Element Core.Msg videoMiniature : Twitch.Playlist -> Twitch.Video -> Element Core.Msg
videoMiniature video = videoMiniature playlist video =
let let
inFront = inFront =
Element.text label Element.text label
@ -229,7 +223,7 @@ videoMiniature video =
, Element.height Element.fill , Element.height Element.fill
, Element.inFront inFront , Element.inFront inFront
] ]
{ description = "", src = Twitch.videoMiniatureUrl video } { description = "", src = Twitch.videoMiniatureUrl playlist video }
label = label =
formatTime video.duration formatTime video.duration
@ -237,12 +231,12 @@ videoMiniature video =
image image
videoMiniatureView : Time.Zone -> Twitch.PlaylistWithVideos -> Twitch.Video -> Element Core.Msg videoMiniatureView : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
videoMiniatureView zone playlist video = videoMiniatureView zone playlist video =
let let
display = display =
Element.column [ Element.width Element.fill, Element.spacing 10 ] Element.column [ Element.width Element.fill, Element.spacing 10 ]
[ videoMiniature video [ videoMiniature playlist video
, videoDescription zone video , videoDescription zone video
] ]
@ -255,7 +249,7 @@ videoMiniatureView zone playlist video =
button button
videoInList : Time.Zone -> Twitch.PlaylistWithVideos -> Twitch.Video -> Twitch.Video -> Element Core.Msg videoInList : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Twitch.Video -> Element Core.Msg
videoInList zone playlist activeVideo video = videoInList zone playlist activeVideo video =
let let
( msg, attr ) = ( msg, attr ) =
@ -274,7 +268,7 @@ videoInList zone playlist activeVideo video =
label = label =
Element.row [ Element.width Element.fill, Element.spacing 10 ] Element.row [ Element.width Element.fill, Element.spacing 10 ]
[ Element.el [ Element.width (Element.fillPortion 2) ] [ Element.el [ Element.width (Element.fillPortion 2) ]
(videoMiniature video) (videoMiniature playlist video)
, Element.el [ Element.width (Element.fillPortion 3), Element.paddingXY 0 10, Element.alignTop ] , Element.el [ Element.width (Element.fillPortion 3), Element.paddingXY 0 10, Element.alignTop ]
(videoDescription zone video) (videoDescription zone video)
] ]
@ -282,7 +276,7 @@ videoInList zone playlist activeVideo video =
Input.button attr { label = label, onPress = msg } Input.button attr { label = label, onPress = msg }
videoView : Element.Device -> Time.Zone -> Twitch.PlaylistWithVideos -> Twitch.Video -> Element Core.Msg videoView : Element.Device -> Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
videoView device zone playlist video = videoView device zone playlist video =
let let
( builder, contentPadding ) = ( builder, contentPadding ) =