diff --git a/apps/api/app/controllers/api/rss_controller.rb b/apps/api/app/controllers/api/rss_controller.rb new file mode 100644 index 0000000..1ed59c3 --- /dev/null +++ b/apps/api/app/controllers/api/rss_controller.rb @@ -0,0 +1,11 @@ +class Api::RssController < ApiController + def blog + xml = RssService.new.blog_feed + render xml: xml, content_type: "application/rss+xml" + end + + def releases + xml = RssService.new.releases_feed + render xml: xml, content_type: "application/rss+xml" + end +end diff --git a/apps/api/app/controllers/downloads_controller.rb b/apps/api/app/controllers/downloads_controller.rb new file mode 100644 index 0000000..00a3bc7 --- /dev/null +++ b/apps/api/app/controllers/downloads_controller.rb @@ -0,0 +1,23 @@ +class DownloadsController < ApplicationController + skip_forgery_protection + + def show + path = params[:path] + return head :bad_request if path.blank? + + full_path = DownloadService.new.call( + path: path, + ip: request.remote_ip, + user_agent: request.user_agent, + referer: request.referer + ) + + if full_path + ext = File.extname(full_path.to_s).delete_prefix(".") + mime = Mime::Type.lookup_by_extension(ext) || "application/octet-stream" + send_file full_path, disposition: "attachment", type: mime + else + head :not_found + end + end +end diff --git a/apps/api/app/models/download.rb b/apps/api/app/models/download.rb new file mode 100644 index 0000000..9b13636 --- /dev/null +++ b/apps/api/app/models/download.rb @@ -0,0 +1,5 @@ +class Download < ApplicationRecord + belongs_to :release, optional: true + + validates :file_path, presence: true +end diff --git a/apps/api/app/models/release.rb b/apps/api/app/models/release.rb index 95a92ec..45f72de 100644 --- a/apps/api/app/models/release.rb +++ b/apps/api/app/models/release.rb @@ -2,6 +2,7 @@ class Release < ApplicationRecord self.table_name = "releases" belongs_to :software + has_many :downloads default_scope { where(deleted_at: nil) } diff --git a/apps/api/app/models/software.rb b/apps/api/app/models/software.rb index c5727e5..87fec1c 100644 --- a/apps/api/app/models/software.rb +++ b/apps/api/app/models/software.rb @@ -4,6 +4,7 @@ class Software < ApplicationRecord has_many :software_images, foreign_key: :software_id, dependent: :destroy has_many :images, through: :software_images has_many :releases, foreign_key: :software_id + has_many :downloads, through: :releases has_many :external_links, foreign_key: :software_id accepts_nested_attributes_for :software_images, allow_destroy: true diff --git a/apps/api/app/serializers/release_serializer.rb b/apps/api/app/serializers/release_serializer.rb index 1231d39..1bb9ee4 100644 --- a/apps/api/app/serializers/release_serializer.rb +++ b/apps/api/app/serializers/release_serializer.rb @@ -14,4 +14,5 @@ class ReleaseSerializer < Blueprinter::Base field(:sourcePath) { |r| r.source_path.blank? ? "" : r.source_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) } field(:htmlFolderPath) { |r| r.html_folder_path.blank? ? "" : r.html_folder_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) } field(:docsFolderPath) { |r| r.docs_folder_path.blank? ? "" : r.docs_folder_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) } + field(:downloadCount) { |r| r.downloads.size } end diff --git a/apps/api/app/serializers/software_detail_serializer.rb b/apps/api/app/serializers/software_detail_serializer.rb index e94cfdd..45b4fe7 100644 --- a/apps/api/app/serializers/software_detail_serializer.rb +++ b/apps/api/app/serializers/software_detail_serializer.rb @@ -3,4 +3,5 @@ class SoftwareDetailSerializer < Blueprinter::Base field(:releases) { |_, opts| ReleaseSerializer.render_as_hash(opts[:releases]) } field(:latestRelease) { |_, opts| opts[:latest] ? ReleaseSerializer.render_as_hash(opts[:latest]) : nil } field(:webPlayableRelease) { |_, opts| opts[:web_playable] ? ReleaseSerializer.render_as_hash(opts[:web_playable]) : nil } + field(:totalDownloads) { |sw, _| Download.where(release: sw.releases).count } end diff --git a/apps/api/app/services/download_service.rb b/apps/api/app/services/download_service.rb new file mode 100644 index 0000000..bbc9bf6 --- /dev/null +++ b/apps/api/app/services/download_service.rb @@ -0,0 +1,21 @@ +class DownloadService + BASE_PATH = Pathname.new(ENV.fetch("FILE_CONTAINER_PATH", "/softwares")).realpath + + def call(path:, ip:, user_agent:, referer:) + full_path = BASE_PATH.join(path.to_s) + return nil unless File.file?(full_path) + + release = Release.find_by("cartridge_path LIKE ? OR source_path LIKE ?", + "%#{path}%", "%#{path}%") + + Download.create!( + file_path: path, + release: release, + ip_address: ip, + user_agent: user_agent&.truncate(500), + referer: referer&.truncate(500) + ) + + full_path + end +end diff --git a/apps/api/app/services/rss_service.rb b/apps/api/app/services/rss_service.rb new file mode 100644 index 0000000..88500db --- /dev/null +++ b/apps/api/app/services/rss_service.rb @@ -0,0 +1,51 @@ +require "rss" + +class RssService + SITE_URL = ENV.fetch("SITE_URL", "https://teletypegames.hu").freeze + + def blog_feed + pages = WikiService.new.pages(tag: "blog", limit: 30) + items = pages.fetch("pages", []) + + RSS::Maker.make("2.0") do |maker| + maker.channel.title = "Teletype Games Blog" + maker.channel.link = "#{SITE_URL}/blog" + maker.channel.description = "Latest blog posts from Teletype Games" + maker.channel.language = "hu" + + items.each do |page| + maker.items.new_item do |item| + item.title = page["title"] + item.link = "#{SITE_URL}/blog/#{page["route"]}" + item.description = page["description"].to_s + item.pubDate = Time.parse(page["createdAt"]) rescue Time.current + end + end + end.to_s + end + + def releases_feed + releases = Release.includes(:software) + .order(created_at: :desc) + .limit(50) + + RSS::Maker.make("2.0") do |maker| + maker.channel.title = "Teletype Games Releases" + maker.channel.link = "#{SITE_URL}/catalog" + maker.channel.description = "Latest game releases from Teletype Games" + maker.channel.language = "hu" + + releases.each do |release| + sw = release.software + next unless sw + + maker.items.new_item do |item| + item.title = "#{sw.title} v#{release.version}" + item.link = "#{SITE_URL}/catalog/#{sw.name}" + item.description = "#{sw.title} #{release.version} released – #{sw.desc}" + item.pubDate = release.created_at + end + end + end.to_s + end +end diff --git a/apps/api/app/services/software_highlighted_service.rb b/apps/api/app/services/software_highlighted_service.rb index 622d287..6fc89bf 100644 --- a/apps/api/app/services/software_highlighted_service.rb +++ b/apps/api/app/services/software_highlighted_service.rb @@ -8,7 +8,7 @@ class SoftwareHighlightedService .first return nil unless software - releases = Release.where(software_id: software.id).to_a + releases = Release.includes(:downloads).where(software_id: software.id).to_a build_response(software, releases) end end diff --git a/apps/api/app/services/software_service.rb b/apps/api/app/services/software_service.rb index 48633e9..7f03f75 100644 --- a/apps/api/app/services/software_service.rb +++ b/apps/api/app/services/software_service.rb @@ -2,7 +2,7 @@ class SoftwareService include SoftwareResponseBuilder def index - softwares = Software.includes(:releases, :external_links, :software_images).all + softwares = Software.includes(releases: :downloads).includes(:external_links, :software_images).all { softwares: softwares.map { |sw| build_response(sw, sw.releases.to_a) } } end end diff --git a/apps/api/config/routes.rb b/apps/api/config/routes.rb index 1d6429d..46a52e9 100644 --- a/apps/api/config/routes.rb +++ b/apps/api/config/routes.rb @@ -9,8 +9,11 @@ Rails.application.routes.draw do get "members", to: "members#index" get "image/:id", to: "images#show" get "wiki/pages", to: "wiki#index" + get "rss/blog", to: "rss#blog" + get "rss/releases", to: "rss#releases" end + get "download", to: "downloads#show" get "update", to: "update#update" get "file/*path", to: "files#show", format: false end diff --git a/apps/api/db/migrate/20260728000001_create_downloads.rb b/apps/api/db/migrate/20260728000001_create_downloads.rb new file mode 100644 index 0000000..d98a5f6 --- /dev/null +++ b/apps/api/db/migrate/20260728000001_create_downloads.rb @@ -0,0 +1,19 @@ +class CreateDownloads < ActiveRecord::Migration[8.1] + def change + create_table :downloads, id: { type: :bigint, unsigned: true }, + charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci" do |t| + t.string :file_path, null: false + t.bigint :release_id, unsigned: true + t.string :ip_address + t.string :user_agent, limit: 500 + t.string :referer, limit: 500 + t.datetime :created_at, precision: 3 + t.datetime :updated_at, precision: 3 + + t.index :file_path + t.index :release_id + end + + add_foreign_key :downloads, :releases, name: "fk_downloads_release" if table_exists?(:releases) + end +end diff --git a/apps/frontend/index.html b/apps/frontend/index.html index 28e3963..3d1db85 100644 --- a/apps/frontend/index.html +++ b/apps/frontend/index.html @@ -6,6 +6,8 @@