Compare commits

...

2 Commits

Author SHA1 Message Date
javara999 79d92160de Add new game badge 2026-06-04 17:59:00 +09:00
javara999 95a7d9a033 Align scheduler with FlaskFarm module flow 2026-05-29 08:30:08 +09:00
4 changed files with 59 additions and 49 deletions
+27 -40
View File
@@ -6,7 +6,7 @@ from datetime import datetime
import requests import requests
from flask import jsonify, render_template from flask import jsonify, render_template
from plugin import PluginModuleBase from plugin import PluginModuleBase
from framework import F, Job, scheduler from framework import F
from .model import ModelFetchLog, ModelFreeGameItem, ModelSetting from .model import ModelFetchLog, ModelFreeGameItem, ModelSetting
from . import scraper from . import scraper
@@ -15,7 +15,6 @@ from .setup import P
logger = P.logger logger = P.logger
package_name = P.package_name package_name = P.package_name
job_id = f"{package_name}_fetch"
_fetch_lock = threading.Lock() _fetch_lock = threading.Lock()
SOURCE_LABELS = { SOURCE_LABELS = {
@@ -99,6 +98,8 @@ def _telegram_send(bot_token, chat_id, games):
class Logic(PluginModuleBase): class Logic(PluginModuleBase):
db_default = { db_default = {
"main_auto_start": "False",
"main_interval": "0 */2 * * *",
"auto_start": "False", "auto_start": "False",
"auto_interval": "0 */2 * * *", "auto_interval": "0 */2 * * *",
"notify_discord_webhook": "", "notify_discord_webhook": "",
@@ -116,18 +117,24 @@ class Logic(PluginModuleBase):
} }
def __init__(self, PM): def __init__(self, PM):
super().__init__(PM, name="main", first_menu="setting") super().__init__(PM, name="main", first_menu="setting", scheduler_desc="FreeGame fetch")
def plugin_load(self): def plugin_load(self):
self._migrate_scheduler_settings()
ModelFreeGameItem.ensure_schema() ModelFreeGameItem.ensure_schema()
if _truthy(ModelSetting.get("auto_start")):
self.scheduler_start() def _migrate_scheduler_settings(self):
legacy_interval = str(ModelSetting.get("auto_interval") or "").strip()
if legacy_interval and str(ModelSetting.get("main_interval") or "").strip() in ["", "0 */2 * * *"]:
ModelSetting.set("main_interval", legacy_interval)
if _truthy(ModelSetting.get("auto_start")) and not _truthy(ModelSetting.get("main_auto_start")):
ModelSetting.set("main_auto_start", "True")
def process_menu(self, sub, req): def process_menu(self, sub, req):
arg = ModelSetting.to_dict() arg = ModelSetting.to_dict()
arg["package_name"] = package_name arg["package_name"] = package_name
arg["scheduler"] = str(F.scheduler.is_include(job_id)) arg["scheduler"] = str(F.scheduler.is_include(self.get_scheduler_name()))
arg["is_running"] = str(F.scheduler.is_running(job_id)) arg["is_running"] = str(F.scheduler.is_running(self.get_scheduler_name()))
arg["source_labels"] = SOURCE_LABELS arg["source_labels"] = SOURCE_LABELS
arg["platform_counts"] = ModelFreeGameItem.get_platform_counts() arg["platform_counts"] = ModelFreeGameItem.get_platform_counts()
if sub == "list": if sub == "list":
@@ -140,21 +147,17 @@ class Logic(PluginModuleBase):
try: try:
if sub == "setting_save": if sub == "setting_save":
ret, _ = ModelSetting.setting_save(req) ret, _ = ModelSetting.setting_save(req)
if F.scheduler.is_include(job_id): self.setting_save_after(None)
Logic.scheduler_stop()
Logic.scheduler_start()
elif _truthy(ModelSetting.get("auto_start")):
Logic.scheduler_start()
ret["ret"] = "success" ret["ret"] = "success"
return jsonify(ret) return jsonify(ret)
if sub == "scheduler_toggle": if sub == "scheduler_toggle":
if req.form["scheduler"] == "true": if req.form["scheduler"] == "true":
Logic.scheduler_start() self.P.logic.scheduler_start(self.name)
else: else:
Logic.scheduler_stop() self.P.logic.scheduler_stop(self.name)
return jsonify({"ret": "success"}) return jsonify({"ret": "success"})
if sub == "execute_once": if sub == "execute_once":
threading.Thread(target=Logic.scheduler_function, daemon=True).start() threading.Thread(target=self.scheduler_function, daemon=True).start()
return jsonify({"ret": "success"}) return jsonify({"ret": "success"})
if sub == "web_list": if sub == "web_list":
ModelFreeGameItem.ensure_schema() ModelFreeGameItem.ensure_schema()
@@ -167,29 +170,14 @@ class Logic(PluginModuleBase):
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return jsonify({"ret": "error", "log": str(e)}) return jsonify({"ret": "error", "log": str(e)})
@staticmethod def setting_save_after(self, change_list):
def scheduler_start(): if F.scheduler.is_include(self.get_scheduler_name()):
try: self.P.logic.scheduler_stop(self.name)
interval = ModelSetting.get("auto_interval") or "0 */2 * * *" self.P.logic.scheduler_start(self.name)
if F.scheduler.is_include(job_id): elif _truthy(ModelSetting.get("main_auto_start")):
scheduler.remove_job(job_id) self.P.logic.scheduler_start(self.name)
job = Job(package_name, job_id, interval, Logic.scheduler_function, "FreeGame fetch", True)
scheduler.add_job_instance(job)
logger.info("FreeGame scheduler registered: id=%s interval=%s", job_id, interval)
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
@staticmethod def scheduler_function(self):
def scheduler_stop():
try:
scheduler.remove_job(job_id)
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
@staticmethod
def scheduler_function():
if not _fetch_lock.acquire(blocking=False): if not _fetch_lock.acquire(blocking=False):
logger.info("FreeGame fetch skipped: already running") logger.info("FreeGame fetch skipped: already running")
return return
@@ -221,7 +209,7 @@ class Logic(PluginModuleBase):
logger.info("FreeGame fetch completed: free_candidates=%d enabled_sources=%d", len(fresh_free_games), len(enabled_sources)) logger.info("FreeGame fetch completed: free_candidates=%d enabled_sources=%d", len(fresh_free_games), len(enabled_sources))
ModelFetchLog("all", "ok", f"free_candidates={len(fresh_free_games)} enabled_sources={len(enabled_sources)}", len(fresh_free_games)).save() ModelFetchLog("all", "ok", f"free_candidates={len(fresh_free_games)} enabled_sources={len(enabled_sources)}", len(fresh_free_games)).save()
ModelSetting.set("last_fetch_finished", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) ModelSetting.set("last_fetch_finished", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
Logic._notify(fresh_free_games) self._notify(fresh_free_games)
except Exception as e: except Exception as e:
logger.error("Exception:%s", e) logger.error("Exception:%s", e)
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@@ -229,8 +217,7 @@ class Logic(PluginModuleBase):
finally: finally:
_fetch_lock.release() _fetch_lock.release()
@staticmethod def _notify(self, games):
def _notify(games):
if _truthy(ModelSetting.get("notify_enabled")) is False: if _truthy(ModelSetting.get("notify_enabled")) is False:
return return
targets = list(games or []) targets = list(games or [])
+14 -3
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
from datetime import datetime from datetime import datetime, timedelta
from sqlalchemy import desc from sqlalchemy import desc
from sqlalchemy import text from sqlalchemy import text
@@ -49,6 +49,7 @@ class ModelFreeGameItem(ModelBase):
return [] return []
def as_dict(self): def as_dict(self):
is_new = bool(self.created_time and self.created_time >= datetime.now() - timedelta(hours=48))
return { return {
"id": self.id, "id": self.id,
"external_id": self.external_id, "external_id": self.external_id,
@@ -66,6 +67,8 @@ class ModelFreeGameItem(ModelBase):
"metacritic_score": self.metacritic_score, "metacritic_score": self.metacritic_score,
"metacritic_url": self.metacritic_url, "metacritic_url": self.metacritic_url,
"genres": self.genres, "genres": self.genres,
"is_new": is_new,
"created_time": self.created_time.strftime("%Y-%m-%d %H:%M:%S") if self.created_time else "",
"updated_time": self.updated_time.strftime("%Y-%m-%d %H:%M:%S") if self.updated_time else "", "updated_time": self.updated_time.strftime("%Y-%m-%d %H:%M:%S") if self.updated_time else "",
} }
@@ -127,8 +130,16 @@ class ModelFreeGameItem(ModelBase):
@classmethod @classmethod
def replace_source_items(cls, source_name, items): def replace_source_items(cls, source_name, items):
with F.app.app_context(): with F.app.app_context():
F.db.session.query(cls).filter_by(platform=source_name).delete(synchronize_session=False) incoming_ids = {
F.db.session.commit() str(item.get("external_id") or "")
for item in items
if str(item.get("external_id") or "")
}
query = F.db.session.query(cls).filter_by(platform=source_name)
if incoming_ids:
query.filter(~cls.external_id.in_(incoming_ids)).delete(synchronize_session=False)
else:
query.delete(synchronize_session=False)
for item in items: for item in items:
cls.upsert(item) cls.upsert(item)
F.db.session.commit() F.db.session.commit()
+11
View File
@@ -83,6 +83,13 @@
background: #16a34a; background: #16a34a;
color: #fff; color: #fff;
} }
.ffg-badge-new {
left: 10px;
bottom: 10px;
background: #ef4444;
color: #fff;
box-shadow: 0 0 0 2px rgba(0, 0, 0, .18);
}
.ffg-platform-epic { background: #2f313a; } .ffg-platform-epic { background: #2f313a; }
.ffg-platform-steam { background: #1b4b91; color: #fff; } .ffg-platform-steam { background: #1b4b91; color: #fff; }
.ffg-platform-gog { background: #4a2876; } .ffg-platform-gog { background: #4a2876; }
@@ -249,6 +256,9 @@ function gameCard(item) {
const mcHtml = mcScore > 0 const mcHtml = mcScore > 0
? `<span class="ffg-mc" title="Metacritic score">MC ${Math.round(mcScore)}</span>` ? `<span class="ffg-mc" title="Metacritic score">MC ${Math.round(mcScore)}</span>`
: ""; : "";
const newHtml = item.is_new
? `<span class="ffg-badge ffg-badge-new">NEW</span>`
: "";
return ` return `
<article class="ffg-card" data-url="${escapeHtml(item.store_url || "")}"> <article class="ffg-card" data-url="${escapeHtml(item.store_url || "")}">
@@ -256,6 +266,7 @@ function gameCard(item) {
${imageHtml} ${imageHtml}
<span class="ffg-badge ffg-badge-platform ${platformClass(item.platform)}">${platformLabel(item.platform)}</span> <span class="ffg-badge ffg-badge-platform ${platformClass(item.platform)}">${platformLabel(item.platform)}</span>
<span class="ffg-badge ffg-badge-free">FREE</span> <span class="ffg-badge ffg-badge-free">FREE</span>
${newHtml}
</div> </div>
<div class="ffg-body"> <div class="ffg-body">
<div class="ffg-title" title="${escapeHtml(item.title)}">${escapeHtml(item.title)}</div> <div class="ffg-title" title="${escapeHtml(item.title)}">${escapeHtml(item.title)}</div>
+7 -6
View File
@@ -3,8 +3,8 @@
<div> <div>
{{ macros.m_button_group([['globalSettingSaveBtn', '설정 저장']]) }} {{ macros.m_button_group([['globalSettingSaveBtn', '설정 저장']]) }}
<form id="setting"> <form id="setting">
{{ macros.setting_input_text('auto_interval', '수집 주기', value=arg.get('auto_interval', '0 */2 * * *'), desc=['기본값: 2시간마다', '크론 표현식 사용']) }} {{ macros.setting_input_text('main_interval', '수집 주기', value=arg.get('main_interval', '0 */2 * * *'), desc=['기본값: 2시간마다', '크론 표현식 사용']) }}
{{ macros.setting_checkbox('auto_start', '시작 시 자동 스케줄 등록', value=arg.get('auto_start', 'False')) }} {{ macros.setting_checkbox('main_auto_start', '시작 시 자동 스케줄 등록', value=arg.get('main_auto_start', 'False')) }}
{{ macros.global_setting_scheduler_button(arg['scheduler'], arg['is_running']) }} {{ macros.global_setting_scheduler_button(arg['scheduler'], arg['is_running']) }}
{{ macros.setting_buttons([['ff_freegame_execute_btn', '1회 실행']], left='수동 실행') }} {{ macros.setting_buttons([['ff_freegame_execute_btn', '1회 실행']], left='수동 실행') }}
{{ macros.info_text('last_fetch_started', '최근 수집 시작', value=arg.get('last_fetch_started', '')) }} {{ macros.info_text('last_fetch_started', '최근 수집 시작', value=arg.get('last_fetch_started', '')) }}
@@ -15,7 +15,7 @@
{{ macros.setting_input_text('notify_telegram_bot_token', 'Telegram Bot Token', value=arg.get('notify_telegram_bot_token', '')) }} {{ macros.setting_input_text('notify_telegram_bot_token', 'Telegram Bot Token', value=arg.get('notify_telegram_bot_token', '')) }}
{{ macros.setting_input_text('notify_telegram_chat_id', 'Telegram Chat ID', value=arg.get('notify_telegram_chat_id', '')) }} {{ macros.setting_input_text('notify_telegram_chat_id', 'Telegram Chat ID', value=arg.get('notify_telegram_chat_id', '')) }}
{{ macros.m_hr() }} {{ macros.m_hr() }}
{{ macros.info_text('source_help', '활성 소스', value='체크 무료 소스만 저장/표시니다.') }} {{ macros.info_text('source_help', '활성 소스', value='체크 무료 소스만 저장/표시니다.') }}
{{ macros.setting_checkbox('source_epic_enabled', 'Epic', value=arg.get('source_epic_enabled', 'True')) }} {{ macros.setting_checkbox('source_epic_enabled', 'Epic', value=arg.get('source_epic_enabled', 'True')) }}
{{ macros.setting_checkbox('source_steam_enabled', 'Steam', value=arg.get('source_steam_enabled', 'True')) }} {{ macros.setting_checkbox('source_steam_enabled', 'Steam', value=arg.get('source_steam_enabled', 'True')) }}
{{ macros.setting_checkbox('source_gog_enabled', 'GOG', value=arg.get('source_gog_enabled', 'True')) }} {{ macros.setting_checkbox('source_gog_enabled', 'GOG', value=arg.get('source_gog_enabled', 'True')) }}
@@ -30,9 +30,9 @@ const package_name = "{{ arg['package_name'] }}";
$("body").on("change", "#globalSchedulerSwitchBtn", function () { $("body").on("change", "#globalSchedulerSwitchBtn", function () {
$.ajax({ $.ajax({
url: `/${package_name}/ajax/scheduler_toggle`, url: `/${package_name}/ajax/scheduler`,
type: "POST", type: "POST",
data: { scheduler: $(this).prop("checked") }, data: { sub: "main", scheduler: $(this).prop("checked") },
dataType: "json", dataType: "json",
}); });
}); });
@@ -40,8 +40,9 @@ $("body").on("change", "#globalSchedulerSwitchBtn", function () {
$("body").on("click", "#ff_freegame_execute_btn", function (e) { $("body").on("click", "#ff_freegame_execute_btn", function (e) {
e.preventDefault(); e.preventDefault();
$.ajax({ $.ajax({
url: `/${package_name}/ajax/execute_once`, url: `/${package_name}/ajax/one_execute`,
type: "POST", type: "POST",
data: { sub: "main" },
dataType: "json", dataType: "json",
success: function (ret) { success: function (ret) {
if (ret.ret === "success") { if (ret.ret === "success") {