obs = obslua ---------------------------------------------------------------- -- GLOBALE EINSTELLUNGEN (STANDARDWERTE) ---------------------------------------------------------------- local settings = { text_source_name = "", prefix = "Nächster Stream", separator = "—", -- Für OBS suffix = "Uhr", day_format = "full", -- "full"=Montag, "short"=Mo max_streams_displayed = 1, even_week_time = "20:00", odd_week_time = "21:00", category_display_mode = "show", -- "show", "show_round", "show_square", "hide" -- Datei-Ausgabe file_path = "", file_use_emojis = true, file_show_category = true, file_max_streams_displayed = 1, file_separator = "—", file_multiline = false, -- true=jede Zeile neuer Stream, false=alle in einer Zeile days = { {enabled = true, auto_time = true, custom_time = "", category = ""}, -- Montag {enabled = false, auto_time = true, custom_time = "", category = ""}, {enabled = false, auto_time = true, custom_time = "", category = ""}, {enabled = false, auto_time = true, custom_time = "", category = ""}, {enabled = false, auto_time = true, custom_time = "", category = ""}, {enabled = false, auto_time = true, custom_time = "", category = ""}, {enabled = false, auto_time = true, custom_time = "", category = ""} } } ---------------------------------------------------------------- -- 1) ISO-8601 KALENDERWOCHE (DONNERSTAGS-REGEL) ---------------------------------------------------------------- local function get_iso_week_number(day_offset) local offset = (day_offset or 0) * 24*60*60 local offset_time = os.time() + offset local wday = tonumber(os.date("%u", offset_time)) -- Montag=1 ... Sonntag=7 local thursday = offset_time + (4 - wday)*24*60*60 local y_thu = os.date("%Y", thursday) local jan1 = os.time{year=y_thu, month=1, day=1} local diff = math.floor((thursday - jan1)/(24*60*60)) return math.floor(diff/7) + 1 end local function is_even_week(w) return (w % 2 == 0) end ---------------------------------------------------------------- -- 2) ZEITFORMAT (24H EINHEITLICH) ---------------------------------------------------------------- local function format_24h(time_str) local stripped = time_str:lower():gsub("[AaPp][Mm]", ""):gsub("%s+", "") local h, m = stripped:match("^(%d+):?(%d*)$") local hh = tonumber(h) or 0 local mm = tonumber(m) or 0 return string.format("%02d:%02d", hh, mm) end ---------------------------------------------------------------- -- 3) WOCHENTAGE ---------------------------------------------------------------- local days_full = {"Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"} local days_short = {"Mo","Di","Mi","Do","Fr","Sa","So"} ---------------------------------------------------------------- -- 4) KATEGORIE WRAPPEN (SHOW, SHOW_ROUND ETC.) -- Wird von OBS und Datei unterschiedlich genutzt ---------------------------------------------------------------- local function wrap_category(cat, mode) if cat == "" then return "" end if mode == "hide" then return "" elseif mode == "show" then return cat elseif mode == "show_round" then return "(" .. cat .. ")" elseif mode == "show_square" then return "[" .. cat .. "]" end return cat end ---------------------------------------------------------------- -- 5) OBS-ZEILE AUFBAUEN -- Ziel: Tag [sep] Kategorie [sep] Zeit + " " + Suffix -- Falls Kategorie leer => Tag [sep] Zeit + " " + Suffix -- Keine doppelten/trailing Separatoren ---------------------------------------------------------------- local function build_obs_line(dayName, cat, timeVal, suffixVal, sep) -- Grundstruktur: "Heute — GTA V — 20:00 Uhr" local line = dayName if cat ~= "" then line = line .. " " .. sep .. " " .. cat end line = line .. " " .. sep .. " " .. timeVal if suffixVal ~= "" then line = line .. " " .. suffixVal end return line end ---------------------------------------------------------------- -- 6) DATEI-ZEILE AUFBAUEN (KORRIGIERT) -- "📅" (optional) + Tag + (optional Kat) + Zeit + (optional Suffix) -- Trennzeichen nur zwischen Tag, Kategorie und Zeit. -- Vor dem Suffix kein Trennzeichen, sondern nur ein Leerzeichen. ---------------------------------------------------------------- local function build_file_line(useEmoji, dayName, cat, timeVal, suffixVal, sep) -- Start mit Emoji + Leerzeichen + Tag local line = "" if useEmoji then line = "📅 " .. dayName else line = dayName end -- Wenn Kategorie nicht leer ist, per Trennzeichen hinzufügen if cat ~= "" then line = line .. sep .. cat end -- Dann immer die Zeit line = line .. sep .. timeVal -- Vor dem Suffix kein sep, nur ein Leerzeichen (falls gewünscht) if suffixVal ~= "" then line = line .. " " .. suffixVal end return line end ---------------------------------------------------------------- -- 7) OBS LISTE ERSTELLEN ---------------------------------------------------------------- local function get_streams_obs() local results = {} local labelSet = (settings.day_format == "full") and days_full or days_short local now = os.time() local tIdx= (os.date("*t").wday + 5) % 7 + 1 local count= 0 for offset = 0, 13 do local idx = (tIdx + offset - 1) % 7 + 1 local dayConf = settings.days[idx] if dayConf.enabled then local wNum = get_iso_week_number(offset) local isE = is_even_week(wNum) local chosen_time = dayConf.auto_time and (isE and settings.even_week_time or settings.odd_week_time) or dayConf.custom_time local tForm = format_24h(chosen_time) local hh, mm = tForm:match("^(%d+):(%d+)$") local stT = os.time{ year = os.date("%Y", now), month = os.date("%m", now), day = os.date("%d", now)+offset, hour = tonumber(hh), min = tonumber(mm) } if stT > now then local dayName = (offset==0 and "Heute") or (offset==1 and "Morgen") or labelSet[idx] local catWrapped = wrap_category(dayConf.category, settings.category_display_mode) local line = build_obs_line(dayName, catWrapped, tForm, settings.suffix, settings.separator) table.insert(results, line) count= count+1 if count >= settings.max_streams_displayed then break end end end end return results end ---------------------------------------------------------------- -- 8) DATEI LISTE ERSTELLEN -- Nur file_show_category => ob cat benutzt wird -- Falls show => wir wrappen cat immer noch gem. OBS-Einstellung -- => "hide" in OBS hat also KEINEN Einfluss auf die Datei ---------------------------------------------------------------- local function get_streams_file() local results= {} local now = os.time() local tIdx= (os.date("*t").wday + 5) % 7 + 1 local count=0 -- Eng->De Map local eng2de = { Monday="Montag", Tuesday="Dienstag", Wednesday="Mittwoch", Thursday="Donnerstag", Friday="Freitag", Saturday="Samstag", Sunday="Sonntag" } for offset = 0, 13 do local idx = (tIdx + offset - 1) % 7 + 1 local dayConf = settings.days[idx] if dayConf.enabled then local wNum = get_iso_week_number(offset) local isE = is_even_week(wNum) local chosen_time = dayConf.auto_time and (isE and settings.even_week_time or settings.odd_week_time) or dayConf.custom_time local tForm = format_24h(chosen_time) local hh, mm = tForm:match("^(%d+):(%d+)$") local stT = os.time{ year = os.date("%Y", now), month = os.date("%m", now), day = os.date("%d", now)+offset, hour = tonumber(hh), min = tonumber(mm) } if stT > now then -- Tag in Deutsch local dayE = os.date("%A", stT) -- z. B. "Monday" local dayD = eng2de[dayE] or dayE if offset==0 then dayD="Heute" elseif offset==1 then dayD="Morgen" end -- Kategorie local finalCat = "" if settings.file_show_category and dayConf.category~="" then -- Ignoriere "hide" => treat as "show" local mode = settings.category_display_mode if mode=="hide" then mode="show" end finalCat = wrap_category(dayConf.category, mode) end local line = build_file_line( settings.file_use_emojis, dayD, finalCat, tForm, settings.suffix, settings.file_separator ) table.insert(results, line) count= count+1 if count >= settings.file_max_streams_displayed then break end end end end return results end ---------------------------------------------------------------- -- 9) OBS & DATEI AKTUALISIEREN ---------------------------------------------------------------- local function update_text_source() -- OBS local obsList = get_streams_obs() local out if #obsList == 0 then out = "Kein Stream geplant" else out = settings.prefix .. "\n" .. table.concat(obsList, "\n") end local src = obs.obs_get_source_by_name(settings.text_source_name) if src then local sset = obs.obs_source_get_settings(src) obs.obs_data_set_string(sset, "text", out) obs.obs_source_update(src, sset) obs.obs_data_release(sset) obs.obs_source_release(src) end -- Datei if settings.file_path == "" then return end local flist = get_streams_file() local fout if #flist == 0 then fout = "Kein Stream geplant" else if settings.file_multiline then -- Jeder Stream in einer neuen Zeile fout = table.concat(flist, "\n") else -- Zusammenfassen in einer Zeile fout = table.concat(flist, settings.file_separator) end end local f = io.open(settings.file_path,"w") if f then f:write(fout) f:close() else obs.script_log(obs.LOG_WARNING,"Konnte Datei nicht öffnen: " .. settings.file_path) end end ---------------------------------------------------------------- -- 10) UI-EIGENSCHAFTEN ---------------------------------------------------------------- local function add_text_sources(p) obs.obs_property_list_add_string(p, "Keine Textquelle gewählt", "") local all_s = obs.obs_enum_sources() if all_s then for _, src in ipairs(all_s) do local sid = obs.obs_source_get_unversioned_id(src) if sid=="text_gdiplus" or sid=="text_ft2_source" then local nm = obs.obs_source_get_name(src) obs.obs_property_list_add_string(p, nm, nm) end end obs.source_list_release(all_s) end end function script_properties() local props = obs.obs_properties_create() -- (1) Textquelle local grp_src = obs.obs_properties_create() local psrc = obs.obs_properties_add_list( grp_src, "text_source_name", "Textquelle auswählen", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) add_text_sources(psrc) obs.obs_properties_add_group(props,"grp_source","📝 Textquelle",obs.OBS_GROUP_NORMAL,grp_src) -- (2) Format (OBS) local grp_fmt = obs.obs_properties_create() obs.obs_properties_add_text(grp_fmt,"prefix","Präfix",obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_text(grp_fmt,"separator","Trennzeichen",obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_text(grp_fmt,"suffix","Suffix",obs.OBS_TEXT_DEFAULT) local df = obs.obs_properties_add_list( grp_fmt,"day_format","Tagesformat (OBS)", obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING ) obs.obs_property_list_add_string(df,"Voll (Montag)","full") obs.obs_property_list_add_string(df,"Kurz (Mo)","short") obs.obs_properties_add_int(grp_fmt,"max_streams_displayed","Wieviele Streams (OBS)",1,14,1) local cdm = obs.obs_properties_add_list( grp_fmt,"category_display_mode","Kategorie-Anzeige (OBS)", obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING ) obs.obs_property_list_add_string(cdm,"Zeigen","show") obs.obs_property_list_add_string(cdm,"Zeigen in ( )","show_round") obs.obs_property_list_add_string(cdm,"Zeigen in [ ]","show_square") obs.obs_property_list_add_string(cdm,"Ausblenden","hide") obs.obs_properties_add_group(props,"grp_fmt_obs","🎨 Format (OBS)",obs.OBS_GROUP_NORMAL,grp_fmt) -- (3) Stream-Start local grp_st = obs.obs_properties_create() obs.obs_properties_add_text(grp_st,"even_week_time","Gerade Woche Start",obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_text(grp_st,"odd_week_time","Ungerade Woche Start",obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_group(props,"grp_streamtimes","🕒 Stream-Start-Einstellungen",obs.OBS_GROUP_NORMAL,grp_st) -- (4) Datei-Ausgabe local grp_file = obs.obs_properties_create() local pth = obs.obs_properties_add_path( grp_file,"file_path","Überschreibe .txt Datei", obs.OBS_PATH_FILE_SAVE,"*.txt",nil ) obs.obs_property_set_long_description(pth,"Die geplanten Streams werden in einer .txt-Datei gespeichert.") obs.obs_properties_add_bool(grp_file,"file_use_emojis","Emojis verwenden?") obs.obs_properties_add_bool(grp_file,"file_show_category","Kategorie anzeigen?") obs.obs_properties_add_int(grp_file,"file_max_streams_displayed","Wieviele Streams (Datei)",1,14,1) obs.obs_properties_add_text(grp_file,"file_separator","Datei-Trennzeichen",obs.OBS_TEXT_DEFAULT) local pmulti = obs.obs_properties_add_bool(grp_file,"file_multiline","Jeder Stream in neuer Zeile?") obs.obs_property_set_long_description(pmulti,"Wenn aktiviert, wird jeder Stream in der TXT auf einer eigenen Zeile ausgegeben. Ansonsten werden alle Streams in einer Zeile gelistet.") obs.obs_properties_add_group(props,"grp_file","📂 Datei-Ausgabe",obs.OBS_GROUP_NORMAL,grp_file) -- (5) Wochentage local dNames = {"Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"} local dKeys = {"monday","tuesday","wednesday","thursday","friday","saturday","sunday"} for i,day in ipairs(dNames) do local dg = obs.obs_properties_create() obs.obs_properties_add_bool(dg, dKeys[i].."_enabled", day.." aktivieren") local tchoice = obs.obs_properties_add_list( dg, dKeys[i].."_time_choice","Stream-Zeit-Auswahl", obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING ) obs.obs_property_list_add_string(tchoice,"Automatisch (gerade/ungerade Woche)","auto") obs.obs_property_list_add_string(tchoice,"Manuell","custom") obs.obs_properties_add_text(dg, dKeys[i].."_custom_time","Manuelle Zeit (HH:MM)",obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_text(dg, dKeys[i].."_category","Kategorie",obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_group(props,"grp_"..dKeys[i],"🗓️ "..day,obs.OBS_GROUP_NORMAL,dg) end return props end ---------------------------------------------------------------- -- 11) SCRIPT UPDATE ---------------------------------------------------------------- function script_update(sett) settings.text_source_name = obs.obs_data_get_string(sett,"text_source_name") or "" settings.prefix = obs.obs_data_get_string(sett,"prefix") or "Nächster Stream" settings.separator = obs.obs_data_get_string(sett,"separator") or "—" settings.suffix = obs.obs_data_get_string(sett,"suffix") or "Uhr" settings.day_format = obs.obs_data_get_string(sett,"day_format") or "full" settings.max_streams_displayed = obs.obs_data_get_int(sett,"max_streams_displayed") settings.category_display_mode = obs.obs_data_get_string(sett,"category_display_mode") or "show" settings.even_week_time = obs.obs_data_get_string(sett,"even_week_time") or "20:00" settings.odd_week_time = obs.obs_data_get_string(sett,"odd_week_time") or "21:00" settings.file_path = obs.obs_data_get_string(sett,"file_path") or "" settings.file_use_emojis = obs.obs_data_get_bool(sett,"file_use_emojis") settings.file_show_category = obs.obs_data_get_bool(sett,"file_show_category") settings.file_max_streams_displayed = obs.obs_data_get_int(sett,"file_max_streams_displayed") settings.file_separator = obs.obs_data_get_string(sett,"file_separator") or "—" settings.file_multiline = obs.obs_data_get_bool(sett,"file_multiline") local dKeys={"monday","tuesday","wednesday","thursday","friday","saturday","sunday"} for i,d in ipairs(dKeys) do settings.days[i].enabled = obs.obs_data_get_bool(sett, d.."_enabled") settings.days[i].auto_time = (obs.obs_data_get_string(sett,d.."_time_choice")=="auto") settings.days[i].custom_time = obs.obs_data_get_string(sett,d.."_custom_time") or "" settings.days[i].category = obs.obs_data_get_string(sett,d.."_category") or "" end update_text_source() end ---------------------------------------------------------------- -- 12) SKRIPT-BESCHREIBUNG & LEBENSZYKLUS ---------------------------------------------------------------- function script_description() return "K_STYER's Dynamic Next Streams (v2.0) zeigt kommende Stream-Termine in einer OBS-Textquelle und optional in einer TXT-Datei an. Es unterscheidet gerade und ungerade Kalenderwochen und ermöglicht individuelle Einstellungen pro Wochentag." end function script_load(sett) script_update(sett) obs.timer_add(update_text_source, 30000) end function script_unload() obs.timer_remove(update_text_source) end