192 lines
6.3 KiB
Julia
192 lines
6.3 KiB
Julia
module WeatherNews
|
|
|
|
using JSON3, HTTP, Dates, TimeZones, DataStructures
|
|
|
|
module API
|
|
include("./api.jl")
|
|
end
|
|
|
|
"""
|
|
The DB module contains code for managing the schedule database.
|
|
"""
|
|
module DB
|
|
include("./db.jl")
|
|
end
|
|
|
|
"Translate Japanese segment titles to short English words."
|
|
TITLES = Dict(
|
|
"ウェザーニュースLiVE・モーニング" => "Morning",
|
|
"ウェザーニュースLiVE・サンシャイン" => "Sunshine",
|
|
"ウェザーニュースLiVE・コーヒータイム" => "Coffee Time",
|
|
"ウェザーニュースLiVE・アフタヌーン" => "Afternoon",
|
|
"ウェザーニュースLiVE・イブニング" => "Evening",
|
|
"ウェザーニュースLiVE・ムーン " => "Moon",
|
|
"ウェザーニュースLiVE" => "_",
|
|
)
|
|
|
|
HOURS = Dict(
|
|
"モーニング" => 5,
|
|
"サンシャイン" => 8,
|
|
"コーヒータイム" => 11,
|
|
"アフタヌーン" => 14,
|
|
"イブニング" => 17,
|
|
"ムーン" => 20
|
|
)
|
|
|
|
function massage_fn(zone, leading_zero; t=now())
|
|
zn = ZonedDateTime(t, localzone())
|
|
td = Date(astimezone(zn, tz"Asia/Tokyo"))
|
|
zero_adjustment = if leading_zero
|
|
0
|
|
else
|
|
1
|
|
end
|
|
ymd = yearmonthday(td)
|
|
return function massage(item)
|
|
if (item[:hour] == "00:00")
|
|
# The closed variable ymd may be mutated.
|
|
ymd = yearmonthday(td + Dates.Day(zero_adjustment))
|
|
zero_adjustment += 1
|
|
end
|
|
hms = (map(i -> replace(i, r"^0" => "") |> n -> parse(Int64, n), split(item[:hour], ":"))..., 0)
|
|
t = ZonedDateTime(ymd..., hms..., tz"Asia/Tokyo")
|
|
n = DataStructures.OrderedDict{Symbol,Any}()
|
|
|
|
n[:caster] = item[:caster]
|
|
n[:title] = TITLES[item[:title]]
|
|
n[:t] = t
|
|
n[:t2] = astimezone(t, zone)
|
|
n[:diff] = n[:t] - zn
|
|
return n
|
|
end
|
|
end
|
|
|
|
# Use a custom time zone instead of the default localzone()
|
|
function get_schedule(zone)
|
|
s = WeatherNews.API.schedule()
|
|
return map(massage_fn(zone, s[1][:hour] == "00:00"), s)
|
|
end
|
|
|
|
# zone - Use custom timezone;
|
|
# s - Provide own JSON response instead of fetching it from the API;
|
|
# t - Optionally specify own now();
|
|
# Use this when doing disaster recovery.
|
|
function get_schedule(zone, s; t=now())
|
|
return map(massage_fn(zone, s[1][:hour] == "00:00", t=t), s)
|
|
end
|
|
|
|
"""
|
|
Fetch the weathernews schedule and translate segment dates to the local time zone.
|
|
|
|
# Examples
|
|
```jldoctest
|
|
julia> s = WeatherNews.get_schedule()
|
|
```
|
|
"""
|
|
function get_schedule()
|
|
get_schedule(localzone())
|
|
end
|
|
|
|
"Derive full jst from stream title's date string and WNL segment title"
|
|
function wnl_title_to_jst(title)
|
|
date_re = r"(?<year>[1-9][0-9]{3})[年.](?<month>[0-1]?[0-9])[月.](?<day>[0-3]?[0-9])日?"
|
|
d = match(date_re, title)
|
|
segment_re = r"モーニング|サンシャイン|コーヒータイム|アフタヌーン|イブニング|ムーン"
|
|
s = match(segment_re, title)
|
|
h = isnothing(s) ? 23 : HOURS[s.match]
|
|
return ZonedDateTime(DateTime(parse(Int64, d[:year]),
|
|
parse(Int64, d[:month]),
|
|
parse(Int64, d[:day]),
|
|
h),
|
|
tz"Asia/Tokyo")
|
|
end
|
|
|
|
"Assume titles with ウェザーニュースLiVE and a full date string belong to regular WNL streams"
|
|
function iswnl(title)
|
|
return occursin(r"ウェザーニュース[Ll][Ii][Vv][Ee]", title) &&
|
|
occursin(r"[1-9][0-9]{3}[年.][0-1]?[0-9][月.][0-3]?[0-9]日?", title)
|
|
end
|
|
|
|
"""
|
|
Fetch upcoming live videos' IDs and update the schedule table's corresponding rows.
|
|
|
|
# Examples
|
|
```jldoctest
|
|
julia> WeatherNews.update_schedule_with_live_video(db)
|
|
```
|
|
"""
|
|
function update_schedule_with_live_video(db)
|
|
vids = WeatherNews.API.video_ids()
|
|
for v in vids
|
|
if iswnl(v[:title])
|
|
jst = wnl_title_to_jst(v[:title])
|
|
DB.update_schedule_with_video(db, string(jst), v[:id])
|
|
if hour(jst) == 23
|
|
DB.update_schedule_with_video(db, string(jst + Hour(1)), v[:id])
|
|
end
|
|
else
|
|
continue
|
|
end
|
|
end
|
|
end
|
|
|
|
"""
|
|
TODO - Given a schedule, check the database for conflicting entries and fix them.
|
|
|
|
This usually happens when a caster has to be rescheduled due to unforseen circumstances.
|
|
"""
|
|
function fix_conflict(db, schedule)
|
|
casters = DB.find_all_casters(db)
|
|
cid = eachrow(casters) |> rows -> map(x -> (x[:n] => x[:id]), rows) |> Dict
|
|
cname = eachrow(casters) |> rows -> map(x -> (x[:id] => x[:n]), rows) |> Dict
|
|
# iterate through schedule
|
|
for s in schedule
|
|
# find a db schedule row with the same jst
|
|
dbs = DB.find_schedule_by_jst(db, string(s[:t]))
|
|
@debug dbs string(s[:t])
|
|
if ismissing(dbs)
|
|
continue
|
|
elseif s[:caster] == "" && !ismissing(dbs[:caster_id])
|
|
# the new caster may be NULL in rare cases
|
|
ts = now(tz"Asia/Tokyo")
|
|
@info "$(ts) :: $(dbs[:id]) no caster"
|
|
DB.cancel_schedule(db, dbs[:id], missing)
|
|
elseif s[:caster] == "" && ismissing(dbs[:caster_id])
|
|
# if they're both null, that's fine. just skip
|
|
@debug "both casters empty"
|
|
continue
|
|
elseif s[:caster] != "" && ismissing(dbs[:caster_id])
|
|
ts = now(tz"Asia/Tokyo")
|
|
@info ("$(ts) :: $(dbs[:id]) NULL replaced with $(s[:caster]):$(cid[s[:caster]])")
|
|
DB.cancel_schedule(db, dbs[:id], cid[s[:caster]])
|
|
elseif cid[s[:caster]] != dbs[:caster_id]
|
|
# if the caster doesn't match,
|
|
# add an entry to the cancellation table
|
|
# update the schedule row with a new caster
|
|
ts = now(tz"Asia/Tokyo")
|
|
@info ("$(ts) :: $(dbs[:id]) $(cname[dbs[:caster_id]]):$(dbs[:caster_id]) replaced by $(s[:caster]):$(cid[s[:caster]])")
|
|
DB.cancel_schedule(db, dbs[:id], cid[s[:caster]])
|
|
else
|
|
@debug "$(dbs[:id]) $(s[:caster]) matches"
|
|
end
|
|
end
|
|
end
|
|
|
|
# Trick LSP so that scripts can see WeatherNews.jl.
|
|
# https://discourse.julialang.org/t/lsp-missing-reference-woes/98231/18
|
|
macro ignore(args...) end
|
|
@ignore include("../bin/weathernews.jl")
|
|
@ignore include("../bin/wndb-insert.jl")
|
|
@ignore include("../bin/wndb-fix-conflict.jl")
|
|
@ignore include("../bin/wndb-video.jl")
|
|
|
|
end
|
|
|
|
|
|
"""
|
|
using WeatherNews
|
|
using WeatherNews: API, DB
|
|
using DataFrames
|
|
s = WeatherNews.get_schedule()
|
|
"""
|