Last active
March 21, 2022 09:51
-
-
Save viartemev/5c90e410a6589a2169f30176965c2663 to your computer and use it in GitHub Desktop.
NHL recap downloader
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"io" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"strings" | |
"sync" | |
"time" | |
) | |
type GameInfo struct { | |
Title string | |
Video string | |
} | |
func main() { | |
start := time.Now() | |
var wg sync.WaitGroup | |
gamesVideos := make(map[int]*GameInfo) | |
upcomingGames := fetchUpcomingGames() | |
finishedGames := filterFinishedGames(upcomingGames) | |
for _, game := range finishedGames { | |
title := fmt.Sprintf("%v vs %v", game.Teams.Home.Team.Name, game.Teams.Away.Team.Name) | |
gamesVideos[game.GamePk] = &GameInfo{title, "video link"} | |
} | |
for _, date := range upcomingGames.Dates { | |
for _, game := range date.Games { | |
wg.Add(1) | |
game := game | |
go func() { | |
gameInfo := fetchGameInfo(game.GamePk) | |
video := extractGameVideo(gameInfo) | |
gamesVideos[game.GamePk] = &GameInfo{Title: gamesVideos[game.GamePk].Title, Video: video} | |
defer wg.Done() | |
}() | |
} | |
} | |
wg.Wait() | |
for _, info := range gamesVideos { | |
fmt.Printf("Game %v %v\n", info.Title, info.Video) | |
} | |
log.Printf("Operations took %s", time.Since(start)) | |
} | |
func filterFinishedGames(schedule Schedule) []Games { | |
var finishedGames []Games | |
for _, date := range schedule.Dates { | |
for _, game := range date.Games { | |
if game.Status.AbstractGameState == "Final" { | |
finishedGames = append(finishedGames, game) | |
} | |
} | |
} | |
return finishedGames | |
} | |
func extractGameVideo(game Game) string { | |
var video string | |
for _, media := range game.Media.Epg { | |
if media.Title == "Recap" { | |
for _, item := range media.Items { | |
for _, playback := range item.Playbacks { | |
if strings.Contains(playback.Name, "FLASH_1800K") { | |
video = playback.Url | |
} | |
} | |
} | |
} | |
} | |
return video | |
} | |
func fetchGameInfo(gamePk int) Game { | |
client := http.Client{ | |
Timeout: time.Second * 2, | |
} | |
url := "https://statsapi.web.nhl.com/api/v1/game/" + fmt.Sprintf("%v", gamePk) + "/content" | |
req, err := http.NewRequest(http.MethodGet, url, nil) | |
if err != nil { | |
log.Fatal(err) | |
} | |
res, getErr := client.Do(req) | |
if getErr != nil { | |
log.Fatal(getErr) | |
} | |
if res.Body != nil { | |
defer func(Body io.ReadCloser) { | |
err := Body.Close() | |
if err != nil { | |
log.Fatal(err) | |
} | |
}(res.Body) | |
} | |
body, readErr := ioutil.ReadAll(res.Body) | |
if readErr != nil { | |
log.Fatal(readErr) | |
} | |
game := &Game{} | |
jsonErr := json.Unmarshal(body, &game) | |
if jsonErr != nil { | |
log.Fatal(jsonErr) | |
} | |
return *game | |
} | |
type Game struct { | |
Link string `json:"link"` | |
Media struct { | |
Epg []struct { | |
Title string `json:"title"` | |
Items []struct { | |
Playbacks []struct { | |
Name string `json:"name"` | |
Url string `json:"url"` | |
} | |
} | |
} | |
} | |
} | |
func fetchUpcomingGames() Schedule { | |
client := http.Client{ | |
Timeout: time.Second * 2, | |
} | |
url := "https://statsapi.web.nhl.com/api/v1/schedule" | |
req, err := http.NewRequest(http.MethodGet, url, nil) | |
if err != nil { | |
log.Fatal(err) | |
} | |
res, getErr := client.Do(req) | |
if getErr != nil { | |
log.Fatal(getErr) | |
} | |
if res.Body != nil { | |
defer func(Body io.ReadCloser) { | |
err := Body.Close() | |
if err != nil { | |
log.Fatal(err) | |
} | |
}(res.Body) | |
} | |
body, readErr := ioutil.ReadAll(res.Body) | |
if readErr != nil { | |
log.Fatal(readErr) | |
} | |
schedule := &Schedule{} | |
jsonErr := json.Unmarshal(body, &schedule) | |
if jsonErr != nil { | |
log.Fatal(jsonErr) | |
} | |
return *schedule | |
} | |
type Schedule struct { | |
Dates []struct { | |
Date string `json:"date"` | |
Games []Games | |
} | |
} | |
type Games struct { | |
GamePk int `json:"gamePk"` | |
Teams Teams | |
Status struct { | |
AbstractGameState string `json:"abstractGameState"` | |
} | |
} | |
type Teams struct { | |
Away struct { | |
Team Team | |
} `json:"away"` | |
Home struct { | |
Team Team | |
} `json:"home"` | |
} | |
type Team struct { | |
Name string `json:"name"` | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.viartemev | |
import com.github.kittinunf.fuel.Fuel | |
import com.github.kittinunf.fuel.core.FuelError | |
import com.github.kittinunf.fuel.jackson.responseObject | |
import com.github.kittinunf.result.Result | |
import com.github.kittinunf.result.flatMap | |
import com.github.kittinunf.result.map | |
import kotlinx.coroutines.async | |
import kotlinx.coroutines.awaitAll | |
import kotlinx.coroutines.coroutineScope | |
import kotlinx.coroutines.runBlocking | |
fun formatMessage(game: Game): String { | |
return "*${game.gameInfo.title}*\n🥅🏒 *${game.gameInfo.score}*\n[Recap](${game.video})" | |
} | |
fun main(args: Array<String>): Unit = runBlocking { | |
Fuel.get("https://statsapi.web.nhl.com/api/v1/schedule") | |
.responseObject<Schedule>() | |
.third | |
.fold({ schedule -> | |
fetchGames(schedule.extractGameInfo()) | |
.map { result -> | |
println("Result: $result") | |
result.map(::formatMessage) } | |
.map { url -> | |
println("Here: $url") | |
url.map { | |
val response = Fuel.get( | |
"https://api.telegram.org/bot742436334:AAFstZitbGd5llQIGJL6-FBKFldg61Nf7_Y/sendMessage", | |
listOf("chat_id" to "-1001256296754", "parse_mode" to "markdown", "text" to it) | |
) | |
.response() | |
println("Code ${response.second.statusCode}") | |
} | |
} | |
}, errorHandler) | |
} | |
val errorHandler: (FuelError) -> Unit = { println("Error during schedule extraction: $it") } | |
suspend fun fetchGames(gamesInfo: List<GameInfo>): List<Result<Game, Exception>> { | |
return coroutineScope { | |
gamesInfo | |
.map { async { it.extractGames() } } | |
.awaitAll() | |
} | |
} | |
fun GameInfo.extractGames() = | |
Fuel.get("https://statsapi.web.nhl.com/api/v1/game/${this.gamePk}/content") | |
.responseObject<GameContent>() | |
.third | |
.flatMap { gameContent -> Result.of(Game(this, gameContent.extractVideo())) } | |
fun GameContent.extractVideo() = this | |
.media | |
.epg | |
.filter { epg -> epg.title == "Recap" } | |
.flatMap { epg -> epg.items.flatMap { item -> item.playbacks ?: emptyList() } } | |
.find { playback -> playback?.name == "FLASH_1800K_960X540" } | |
?.url | |
fun Schedule.extractGameInfo() = this | |
.dates | |
.flatMap { date -> | |
date.games.map { game -> | |
GameInfo( | |
date.date, | |
"${game.teams.away.team.name} - ${game.teams.home.team.name}", | |
"${game.teams.away.score} - ${game.teams.home.score}", | |
game.gamePk | |
) | |
} | |
} | |
data class GameInfo( | |
val date: String, | |
val title: String, | |
val score: String, | |
val gamePk: Long | |
) | |
data class Game( | |
val gameInfo: GameInfo, | |
val video: String? | |
) { | |
override fun toString(): String { | |
return """ | |
${gameInfo.date} \n ${gameInfo.title} \n ${gameInfo.score} \n $video | |
""".trimIndent() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment