elm-twitch/src/Twitch.elm

271 lines
6.5 KiB
Elm
Raw Normal View History

2020-10-04 13:15:57 +02:00
module Twitch exposing
( Playlist
2020-10-04 23:03:38 +02:00
, PlaylistWithVideos
2020-10-04 13:15:57 +02:00
, Video
2020-10-04 23:03:38 +02:00
, fetchPlaylistWithVideos
2020-10-04 13:15:57 +02:00
, fetchPlaylists
, playlistMiniatureUrl
2020-10-04 17:06:03 +02:00
, videoId
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 23:03:38 +02:00
{ url : String
, name : String
, videos : List String
}
type alias PlaylistWithVideos =
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 18:16:15 +02:00
, date : Maybe 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-04 17:06:03 +02:00
videoId : Video -> String
videoId video =
String.dropLeft 1 video.url |> String.replace "/" "-"
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 ->
2020-10-04 23:03:38 +02:00
"videos/" ++ playlist.url ++ v ++ "miniature-050.png"
2020-10-04 13:15:57 +02:00
_ ->
""
2020-10-04 14:24:16 +02:00
videoMiniatureUrl : Video -> String
videoMiniatureUrl video =
video.url ++ "miniature-050.png"
sortPlaylist : Playlist -> Playlist
sortPlaylist playlist =
2020-10-04 23:03:38 +02:00
{ playlist | videos = List.sort playlist.videos |> List.reverse }
2020-10-04 14:24:16 +02:00
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
2020-10-04 23:03:38 +02:00
-- |> Task.andThen (\c -> fetchVideos name c)
2020-10-04 13:15:57 +02:00
|> Task.map (Playlist name a)
2020-10-03 19:39:45 +02:00
)
2020-10-04 23:03:38 +02:00
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)
)
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 18:16:15 +02:00
Ok { name = "", url = url, duration = 0, date = Nothing }
2020-10-03 19:39:45 +02:00
_ ->
2020-10-04 18:16:15 +02:00
Ok { name = "", url = url, duration = 0, date = Nothing }
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 18:53:54 +02:00
(Decode.maybe (Decode.field "date" Iso8601.decoder))