diff --git a/.gitignore b/.gitignore index 5053d72..ebb8f2d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ js/main.js js/main.tmp.js js/main.min.js deploy.sh +index.json diff --git a/index.html b/index.html index be6e215..4a9f4e5 100644 --- a/index.html +++ b/index.html @@ -37,6 +37,12 @@ }); }); } + + if (app.ports !== undefined && app.ports.eraseVideo !== undefined) { + app.ports.eraseVideo.subscribe(function() { + lastId = undefined; + }); + } diff --git a/indexify.js b/indexify.js new file mode 100644 index 0000000..788f3e7 --- /dev/null +++ b/indexify.js @@ -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)); + diff --git a/src/Core.elm b/src/Core.elm index 9f2c5af..6c021f9 100644 --- a/src/Core.elm +++ b/src/Core.elm @@ -4,8 +4,8 @@ import Browser.Events as Events import Browser.Navigation as Nav import Dict exposing (Dict) import Element +import Http import Ports -import Task import Time import Twitch import Url @@ -27,19 +27,16 @@ type alias Model = type Page = Home - | Loading - | Playlist Twitch.PlaylistWithVideos - | Video Twitch.PlaylistWithVideos Twitch.Video + | Playlist Twitch.Playlist + | Video Twitch.Playlist Twitch.Video type Msg = Noop | PlaylistsReceived ( List Twitch.Playlist, Time.Zone ) | HomeClicked - | PlaylistClicked String - | PlaylistReceived Twitch.PlaylistWithVideos - | PlaylistReceivedVideo String (Maybe Int) Twitch.PlaylistWithVideos - | VideoClicked Twitch.PlaylistWithVideos Twitch.Video + | PlaylistClicked Twitch.Playlist + | VideoClicked Twitch.Playlist Twitch.Video | UrlReceived Url.Url | SizeReceived Int Int @@ -47,10 +44,20 @@ type Msg init : { width : Int, height : Int } -> Url.Url -> Nav.Key -> ( FullModel, Cmd Msg ) init { width, 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 _ = Events.onResize (\w h -> SizeReceived w h) @@ -79,12 +86,12 @@ update msg model = ( PlaylistClicked playlist, Loaded m ) -> ( model - , Nav.pushUrl m.key ("#" ++ playlist) + , Nav.pushUrl m.key ("#" ++ playlist.url) ) ( VideoClicked playlist video, Loaded m ) -> ( model - , Nav.pushUrl m.key ("#" ++ playlist.url ++ Twitch.videoName video) + , Nav.pushUrl m.key ("#" ++ playlist.url ++ video.url) ) ( UrlReceived url, Loaded m ) -> @@ -95,14 +102,10 @@ update msg model = ( split, args ) = case splits of h1 :: h2 :: _ -> - ( String.split "/" h1 |> List.filter (not << String.isEmpty) - , parseQueryString h2 - ) + ( String.split "/" h1, parseQueryString h2 ) h1 :: _ -> - ( String.split "/" h1 |> List.filter (not << String.isEmpty) - , Dict.empty - ) + ( String.split "/" h1, Dict.empty ) _ -> ( [], Dict.empty ) @@ -130,72 +133,38 @@ update msg model = ( Nothing, Nothing ) playlist = - case m.page of - Playlist p -> - Just p + List.head (List.filter (\x -> Just x.url == playlistName) m.playlists) - Video p _ -> - Just p - - _ -> - Nothing - - realVideo = + video = case playlist of Just p -> - case List.head (List.filter (\x -> Just (Twitch.videoName x) == videoName) p.videos) of - Just v -> - Just v - - _ -> - Nothing + List.head (List.filter (\x -> Just x.url == videoName) p.videos) _ -> Nothing ( page, cmd ) = - case ( ( playlist, realVideo ), ( playlistName, videoName ) ) of - ( ( Just p, _ ), ( Just _, Nothing ) ) -> - ( Playlist p - , Cmd.none + case ( playlist, video ) of + ( Just p, Just v ) -> + ( Video p v + , Ports.registerVideo ( Twitch.videoId v, "videos/" ++ p.url ++ v.url, time ) ) - ( ( Just p, Just video ), ( Just _, Just _ ) ) -> - ( Video p video - , 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) - ) + ( Just p, Nothing ) -> + ( Playlist p, Cmd.none ) _ -> ( Home, Cmd.none ) + + extraCmd = + case page of + Video _ _ -> + Cmd.none + + _ -> + Ports.eraseVideo () in - ( Loaded { m | page = page }, cmd ) - - ( 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 ) + ( Loaded { m | page = page }, Cmd.batch [ cmd, extraCmd ] ) _ -> ( model, Cmd.none ) diff --git a/src/Ports.elm b/src/Ports.elm index 45e3bbc..cdf4d98 100644 --- a/src/Ports.elm +++ b/src/Ports.elm @@ -1,4 +1,7 @@ -port module Ports exposing (registerVideo) +port module Ports exposing (eraseVideo, registerVideo) port registerVideo : ( String, String, Maybe Int ) -> Cmd msg + + +port eraseVideo : () -> Cmd msg diff --git a/src/Twitch.elm b/src/Twitch.elm index 25d4f6b..81424e6 100644 --- a/src/Twitch.elm +++ b/src/Twitch.elm @@ -1,8 +1,7 @@ module Twitch exposing ( Playlist - , PlaylistWithVideos , Video - , fetchPlaylistWithVideos + , decodePlaylists , fetchPlaylists , playlistMiniatureUrl , videoId @@ -10,28 +9,12 @@ module Twitch exposing , videoName ) -import Html.Parser import Http import Iso8601 import Json.Decode as Decode -import Task exposing (Task) 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 = { name : 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.join "/" (List.drop 3 (String.split "/" video.url)) + video.url videoId : Video -> String @@ -50,222 +70,16 @@ videoId video = 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 = case List.head (List.reverse playlist.videos) of Just v -> - "videos/" ++ playlist.url ++ v ++ "miniature-050.png" + "videos/" ++ playlist.url ++ v.url ++ "miniature-050.png" _ -> "" -videoMiniatureUrl : Video -> String -videoMiniatureUrl video = - 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 " 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)) +videoMiniatureUrl : Playlist -> Video -> String +videoMiniatureUrl playlist video = + "videos/" ++ playlist.url ++ video.url ++ "miniature-050.png" diff --git a/src/Views.elm b/src/Views.elm index e0d620b..94110e6 100644 --- a/src/Views.elm +++ b/src/Views.elm @@ -55,9 +55,6 @@ title model = Core.Home -> Consts.url - Core.Loading -> - Consts.url - Core.Playlist p -> Consts.url ++ " - " ++ p.name @@ -71,9 +68,6 @@ viewContent model = Core.Home -> playlistsView model.device model.playlists - Core.Loading -> - Element.el [ Element.padding 10, Element.centerX ] spinner - Core.Playlist playlist -> videoMiniaturesView model.device model.zone playlist @@ -177,13 +171,13 @@ playlistView playlist = button = Input.button [ Element.width Element.fill, Element.alignTop ] { label = display - , onPress = Just (Core.PlaylistClicked playlist.url) + , onPress = Just (Core.PlaylistClicked playlist) } in 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 = let empty = @@ -206,8 +200,8 @@ videoMiniaturesView device zone playlist = final -videoMiniature : Twitch.Video -> Element Core.Msg -videoMiniature video = +videoMiniature : Twitch.Playlist -> Twitch.Video -> Element Core.Msg +videoMiniature playlist video = let inFront = Element.text label @@ -229,7 +223,7 @@ videoMiniature video = , Element.height Element.fill , Element.inFront inFront ] - { description = "", src = Twitch.videoMiniatureUrl video } + { description = "", src = Twitch.videoMiniatureUrl playlist video } label = formatTime video.duration @@ -237,12 +231,12 @@ videoMiniature video = 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 = let display = Element.column [ Element.width Element.fill, Element.spacing 10 ] - [ videoMiniature video + [ videoMiniature playlist video , videoDescription zone video ] @@ -255,7 +249,7 @@ videoMiniatureView zone playlist video = 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 = let ( msg, attr ) = @@ -274,7 +268,7 @@ videoInList zone playlist activeVideo video = label = Element.row [ Element.width Element.fill, Element.spacing 10 ] [ 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 ] (videoDescription zone video) ] @@ -282,7 +276,7 @@ videoInList zone playlist activeVideo video = 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 = let ( builder, contentPadding ) =