2020-10-04 13:15:57 +02:00
|
|
|
module Twitch exposing
|
|
|
|
( Playlist
|
|
|
|
, Video
|
|
|
|
, fetchPlaylists
|
|
|
|
, playlistMiniatureUrl
|
2020-10-04 14:24:16 +02:00
|
|
|
, videoMiniatureUrl
|
2020-10-04 16:02:54 +02:00
|
|
|
, videoName
|
2020-10-04 13:15:57 +02:00
|
|
|
)
|
2020-10-03 18:44:16 +02:00
|
|
|
|
|
|
|
import Html.Parser
|
|
|
|
import Http
|
2020-10-04 15:24:04 +02:00
|
|
|
import Iso8601
|
2020-10-03 18:44:16 +02:00
|
|
|
import Json.Decode as Decode
|
|
|
|
import Task exposing (Task)
|
2020-10-04 15:24:04 +02:00
|
|
|
import Time
|
2020-10-03 18:44:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
type alias Playlist =
|
2020-10-04 13:15:57 +02:00
|
|
|
{ url : String
|
|
|
|
, name : String
|
2020-10-03 18:44:16 +02:00
|
|
|
, videos : List Video
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias Video =
|
|
|
|
{ name : String
|
|
|
|
, url : String
|
2020-10-04 13:15:57 +02:00
|
|
|
, duration : Int
|
2020-10-04 15:24:04 +02:00
|
|
|
, date : Time.Posix
|
2020-10-03 18:44:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-10-04 16:02:54 +02:00
|
|
|
videoName : Video -> String
|
|
|
|
videoName video =
|
|
|
|
String.join "/" (List.drop 3 (String.split "/" video.url))
|
|
|
|
|
|
|
|
|
2020-10-03 18:44:16 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-10-04 13:15:57 +02:00
|
|
|
playlistMiniatureUrl : Playlist -> String
|
|
|
|
playlistMiniatureUrl playlist =
|
|
|
|
case List.head playlist.videos of
|
|
|
|
Just v ->
|
|
|
|
v.url ++ "miniature-050.png"
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
""
|
|
|
|
|
|
|
|
|
2020-10-04 14:24:16 +02:00
|
|
|
videoMiniatureUrl : Video -> String
|
|
|
|
videoMiniatureUrl video =
|
|
|
|
video.url ++ "miniature-050.png"
|
|
|
|
|
|
|
|
|
|
|
|
sortPlaylist : Playlist -> Playlist
|
|
|
|
sortPlaylist playlist =
|
|
|
|
{ playlist | videos = List.sortBy .url playlist.videos |> List.reverse }
|
|
|
|
|
|
|
|
|
|
|
|
sortPlaylists : List Playlist -> List Playlist
|
|
|
|
sortPlaylists playlists =
|
|
|
|
List.sortBy .url (List.map sortPlaylist playlists) |> List.reverse
|
|
|
|
|
|
|
|
|
2020-10-04 15:24:04 +02:00
|
|
|
fetchPlaylists : Task x ( List Playlist, Time.Zone )
|
2020-10-03 18:44:16 +02:00
|
|
|
fetchPlaylists =
|
2020-10-04 15:24:04 +02:00
|
|
|
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
|
2020-10-03 18:44:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
fetchPlaylistPath : Task x (List String)
|
|
|
|
fetchPlaylistPath =
|
|
|
|
get
|
|
|
|
{ url = "/videos"
|
2020-10-04 13:15:57 +02:00
|
|
|
, resolver = Http.stringResolver parseHrefs
|
2020-10-03 18:44:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fetchPlaylist : String -> Task x Playlist
|
|
|
|
fetchPlaylist name =
|
2020-10-04 13:15:57 +02:00
|
|
|
fetchPlaylistName name
|
2020-10-03 19:39:45 +02:00
|
|
|
|> Task.andThen
|
|
|
|
(\a ->
|
2020-10-04 13:15:57 +02:00
|
|
|
fetchPlaylistVideoPaths name
|
|
|
|
|> Task.andThen (\c -> fetchVideos name c)
|
|
|
|
|> Task.map (Playlist name a)
|
2020-10-03 19:39:45 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
fetchVideo : String -> String -> Task x Video
|
|
|
|
fetchVideo playlist video =
|
|
|
|
let
|
|
|
|
url =
|
|
|
|
"/videos/" ++ playlist ++ video
|
|
|
|
in
|
|
|
|
get
|
2020-10-04 13:15:57 +02:00
|
|
|
{ url = url ++ "description.json"
|
2020-10-03 19:39:45 +02:00
|
|
|
, resolver = Http.stringResolver (parseVideo url)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fetchVideos : String -> List String -> Task x (List Video)
|
|
|
|
fetchVideos playlist videos =
|
|
|
|
Task.sequence (List.map (fetchVideo playlist) videos)
|
|
|
|
|
|
|
|
|
2020-10-04 13:15:57 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-10-03 19:39:45 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
_ ->
|
2020-10-04 15:24:04 +02:00
|
|
|
Ok { name = "", url = url, duration = 0, date = Time.millisToPosix 0 }
|
2020-10-03 19:39:45 +02:00
|
|
|
|
|
|
|
_ ->
|
2020-10-04 15:24:04 +02:00
|
|
|
Ok { name = "", url = url, duration = 0, date = Time.millisToPosix 0 }
|
2020-10-03 18:44:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
fetchPlaylistsMapper : List String -> Task x (List Playlist)
|
|
|
|
fetchPlaylistsMapper names =
|
|
|
|
Task.sequence (List.map fetchPlaylist names)
|
|
|
|
|
|
|
|
|
2020-10-03 19:39:45 +02:00
|
|
|
parsePlaylistName : Http.Response String -> Result x String
|
|
|
|
parsePlaylistName result =
|
2020-10-03 18:44:16 +02:00
|
|
|
case result of
|
|
|
|
Http.GoodStatus_ _ content ->
|
2020-10-03 19:39:45 +02:00
|
|
|
case Decode.decodeString decodePlaylistName content of
|
2020-10-03 18:44:16 +02:00
|
|
|
Ok p ->
|
|
|
|
Ok p
|
|
|
|
|
|
|
|
_ ->
|
2020-10-03 19:39:45 +02:00
|
|
|
Ok ""
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
Ok ""
|
|
|
|
|
|
|
|
|
2020-10-04 13:15:57 +02:00
|
|
|
parseHrefs : Http.Response String -> Result x (List String)
|
|
|
|
parseHrefs result =
|
2020-10-03 18:44:16 +02:00
|
|
|
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 =
|
2020-10-04 14:24:16 +02:00
|
|
|
if key == "href" && String.endsWith "/" value && value /= "../" then
|
2020-10-03 18:44:16 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2020-10-03 19:39:45 +02:00
|
|
|
decodePlaylistName : Decode.Decoder String
|
|
|
|
decodePlaylistName =
|
|
|
|
Decode.field "title" Decode.string
|
|
|
|
|
|
|
|
|
|
|
|
decodeVideo : String -> Decode.Decoder Video
|
|
|
|
decodeVideo url =
|
2020-10-04 15:24:04 +02:00
|
|
|
Decode.map3 (\x y -> Video x url y)
|
2020-10-03 19:39:45 +02:00
|
|
|
(Decode.field "title" Decode.string)
|
2020-10-04 13:15:57 +02:00
|
|
|
(Decode.map Basics.round (Decode.field "duration" Decode.float))
|
2020-10-04 15:24:04 +02:00
|
|
|
(Decode.field "date" Iso8601.decoder)
|