WeatherNews.jl/src/WeatherNews.jl

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()
"""