diff --git a/.env.testing b/.env.testing
index 4737a59..1d4e1d8 100644
--- a/.env.testing
+++ b/.env.testing
@@ -1,5 +1,5 @@
DATABASE_HOST=127.0.0.1
-DATABASE_PORT=3306
+DATABASE_PORT=5432
DATABASE_USERNAME=vapor_username
DATABASE_PASSWORD=vapor_password
DATABASE_NAME=test_agqr_program_guide
diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml
index 29b25ea..742d2f8 100644
--- a/.github/workflows/build-push.yml
+++ b/.github/workflows/build-push.yml
@@ -1,4 +1,5 @@
name: build-push
+run-name: Push [${{ inputs.version }}] from ${{ github.ref_name }}
on:
workflow_dispatch:
@@ -12,18 +13,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v1.5.1
+ uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
- uses: docker/login-action@v1
+ uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
- uses: docker/build-push-action@v2
+ uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.event.inputs.version }}
diff --git a/.github/workflows/check-openapi.yaml b/.github/workflows/check-openapi.yaml
new file mode 100644
index 0000000..8f56b0b
--- /dev/null
+++ b/.github/workflows/check-openapi.yaml
@@ -0,0 +1,34 @@
+name: Check OpenAPI
+on:
+ push:
+ branches:
+ - master
+ paths:
+ - '.github/workflows/check-openapi.yaml'
+ - 'reference/agqr-radio-program-guide-api.v2.yaml'
+ - 'Resources/Views/redoc-static.html'
+ pull_request:
+ branches:
+ - master
+ paths:
+ - '.github/workflows/check-openapi.yaml'
+ - 'reference/agqr-radio-program-guide-api.v2.yaml'
+ - 'Resources/Views/redoc-static.html'
+
+jobs:
+ exists-diff:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - run: make generate-redoc
+ - name: Check exists diff
+ run: |
+ if ! git diff --quiet; then
+ exit 1
+ fi
+ # TODO: 後で直す
+ # lint:
+ # runs-on: ubuntu-latest
+ # steps:
+ # - uses: actions/checkout@v4
+ # - run: make lint-openapi
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index eafcd2a..7e49897 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -6,10 +6,23 @@ on:
- master
jobs:
- setup:
- runs-on: macos-latest
+ build:
+ name: Swift ${{ matrix.swift }} on ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ swift: ["5.9"]
+ runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v2
+ - uses: swift-actions/setup-swift@v1
+ with:
+ swift-version: ${{ matrix.swift }}
+ - uses: actions/checkout@v4
- run: swift package resolve
- - name: swift test
+ - name: Build
+ run: swift build
+ - name: setup service
+ run: |
+ docker compose up -d db --wait
+ - name: Run tests
run: swift test
diff --git a/Dockerfile b/Dockerfile
index aae292c..f6de261 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -60,4 +60,4 @@ EXPOSE 8080
# Start the Vapor service when the image is run, default to listening on 8080 in production environment
ENTRYPOINT ["./Run"]
-CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
\ No newline at end of file
+CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
diff --git a/Makefile b/Makefile
index 9156d94..bc99607 100644
--- a/Makefile
+++ b/Makefile
@@ -21,6 +21,11 @@ migrate:
build:
docker-compose build --no-cache
up:
- docker-compose up -d db redis
+ docker-compose up -d db
down:
docker-compose down
+
+.PHONY: generate-redoc
+generate-redoc:
+ docker run --rm -v ${PWD}/reference:/spec redocly/cli build-docs agqr-radio-program-guide-api.v2.yaml -o redoc-static.html
+ mv reference/redoc-static.html Resources/Views/redoc-static.html
diff --git a/Package.resolved b/Package.resolved
index 4131c0e..a4b0cf7 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -47,12 +47,12 @@
}
},
{
- "package": "fluent-mysql-driver",
- "repositoryURL": "https://github.com/vapor/fluent-mysql-driver.git",
+ "package": "fluent-postgres-driver",
+ "repositoryURL": "https://github.com/vapor/fluent-postgres-driver.git",
"state": {
"branch": null,
- "revision": "f86bf9c80a1c176234a16796c19add399a266c38",
- "version": "4.0.2"
+ "revision": "7c266b539f71331ad6e53ea8fae587ccdaf972f2",
+ "version": "2.2.6"
}
},
{
@@ -92,57 +92,21 @@
}
},
{
- "package": "mysql-kit",
- "repositoryURL": "https://github.com/vapor/mysql-kit.git",
+ "package": "postgres-kit",
+ "repositoryURL": "https://github.com/vapor/postgres-kit.git",
"state": {
"branch": null,
- "revision": "f54e0876fce2d68551be0e704b3a94f78eedb9f8",
- "version": "4.5.0"
+ "revision": "35deea5c28a7d402f3280d81dee37bed5c56b9fe",
+ "version": "2.8.3"
}
},
{
- "package": "mysql-nio",
- "repositoryURL": "https://github.com/vapor/mysql-nio.git",
+ "package": "postgres-nio",
+ "repositoryURL": "https://github.com/vapor/postgres-nio.git",
"state": {
"branch": null,
- "revision": "f0e8ad7e18e870e8665311d70bf3f01dcd6024a8",
- "version": "1.4.0"
- }
- },
- {
- "package": "queues",
- "repositoryURL": "https://github.com/vapor/queues.git",
- "state": {
- "branch": null,
- "revision": "58b2d785118d164b38c59beb595cbffbea7608ef",
- "version": "1.8.1"
- }
- },
- {
- "package": "queues-redis-driver",
- "repositoryURL": "https://github.com/vapor/queues-redis-driver.git",
- "state": {
- "branch": null,
- "revision": "2728477b50e24be82f5bc0bd0722c35656e1c5b1",
- "version": "1.0.3"
- }
- },
- {
- "package": "redis",
- "repositoryURL": "https://github.com/vapor/redis.git",
- "state": {
- "branch": null,
- "revision": "e955843b08064071f465a6b1ca9e04bebad8623a",
- "version": "4.6.0"
- }
- },
- {
- "package": "RediStack",
- "repositoryURL": "https://gitlab.com/mordil/RediStack.git",
- "state": {
- "branch": null,
- "revision": "5458d6476e05d5f1b43097f1bc9b599e936b5f2f",
- "version": "1.3.0"
+ "revision": "d648c5b4594ffbc2f6173318f70f5531e05ccb4e",
+ "version": "1.11.0"
}
},
{
@@ -163,6 +127,15 @@
"version": "3.18.0"
}
},
+ {
+ "package": "swift-atomics",
+ "repositoryURL": "https://github.com/apple/swift-atomics.git",
+ "state": {
+ "branch": null,
+ "revision": "cd142fd2f64be2100422d658e7411e39489da985",
+ "version": "1.2.0"
+ }
+ },
{
"package": "swift-backtrace",
"repositoryURL": "https://github.com/swift-server/swift-backtrace.git",
diff --git a/Package.swift b/Package.swift
index a5a1534..b32d0a4 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,19 +1,17 @@
-// swift-tools-version:5.2
+// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "agqr-program-guide",
platforms: [
- .macOS(.v10_15)
+ .macOS(.v12)
],
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
- .package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.0.0"),
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
.package(url: "https://github.com/vapor/leaf.git", from: "4.0.0"),
- .package(url: "https://github.com/vapor/redis.git", from: "4.0.0"),
- .package(url: "https://github.com/vapor/queues-redis-driver.git", from: "1.0.0"),
// HTML Parser
.package(url: "https://github.com/tid-kijyun/Kanna.git", .upToNextMajor(from: "5.2.4"))
],
@@ -22,10 +20,8 @@ let package = Package(
name: "App",
dependencies: [
.product(name: "Fluent", package: "fluent"),
- .product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"),
+ .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
.product(name: "Leaf", package: "leaf"),
- .product(name: "Redis", package: "redis"),
- .product(name: "QueuesRedisDriver", package: "queues-redis-driver"),
.product(name: "Vapor", package: "vapor"),
.product(name: "Kanna", package: "Kanna"),
diff --git a/README.md b/README.md
index b42649d..6d33fa8 100644
--- a/README.md
+++ b/README.md
@@ -1,62 +1,7 @@
# agqr-program-guide
-[超A&G+の番組表](https://www.joqr.co.jp/qr/agdailyprogram/) から番組情報・出演者を取得し、JSON形式のREST APIとして提供しています。
+[超A&G+の番組表](https://www.joqr.co.jp/qr/agdailyprogram/) から番組情報・出演者を取得し、JSON 形式の REST API として提供しています。
https://agqr.sun-yryr.com で提供中です。
-フレームワークとしてSwift製のVaporを利用しています。
-
-## Development
-
-前提
-```bash
-swift --version
-# 5.4.2
-vapor --version
-# framework: 4.45.0
-# toolbox: 18.3.3
-```
-
-1. DBとRedisを起動する。 `make up`
-1. 開発する。
-1. サーバーを立てて動作確認する。 `make serve`
-
-### lint
-
-swiftlintかswift-formatを利用する予定。更新する。
-
-### build & push
-
-GitHub Actionsのページから `build-push` のワークフローを実行する。
-
-### deploy
-
-1. EC2にログインする。
-1. pushしたDockerImageをPullしてくる。(直接起動する場合は飛ばしてもいい)
-1. 環境変数とかを設定して起動する。下記の「使う」を参考にする。
-
-## 使う
-
-適宜タグや環境変数の変更が必要。
-
-### マイグレーション
-```bash
-docker run --rm -e DATABASE_HOST=127.0.0.1 -e REDIS_URL="redis://127.0.0.1:6379" ghcr.io/sun-yryr/agqr-program-guide:latest migrate --yes
-```
-
-### 手動スクレイピング
-
-基本的に自動で取ってくるので必要ない。初回起動などで利用する。
-※ hoge は使っていない引数なので後々削除する。
-```bash
-docker run --rm -e DATABASE_HOST=127.0.0.1 -e REDIS_URL="redis://127.0.0.1:6379" -e TZ=Asia/Tokyo ghcr.io/sun-yryr/agqr-program-guide:latest scraping hoge
-```
-
-### 起動
-```bash
-docker run -d -e DATABASE_HOST=127.0.0.1 -e REDIS_URL="redis://127.0.0.1:6379" -e TZ=Asia/Tokyo -p 3000:8080 ghcr.io/sun-yryr/agqr-program-guide:latest
-```
-
-## ライセンス
-
-そのうち
+フレームワークとして [Vapor](https://github.com/vapor/vapor) を利用しています。
diff --git a/Resources/Views/redoc-static.html b/Resources/Views/redoc-static.html
new file mode 100644
index 0000000..2b56195
--- /dev/null
+++ b/Resources/Views/redoc-static.html
@@ -0,0 +1,338 @@
+
+
+
+
+
+ agqr-radio-program-guide-api
+
+
+
+
+
+
+
+
+
+ agqr-radio-program-guide-api (1.0)
Download OpenAPI specification:Download
週刊番組表を取得
1週間分全ての番組情報を返却する。
+実行日の日本時間の0時から7日後の23時59分までの間の番組情報を返却する。
+
https://agqr.sun-yryr.com/api/all
http://localhost:8080/api/all
Response samples
Content typeapplication/json
[{"title": "string",
"ft": "2019-08-24T14:15:22Z",
"to": "2019-08-24T14:15:22Z",
"pfm": "string",
"dur": 0,
"isBroadcast": true,
"isRepeat": true
}
]
日間番組表を取得
本日放送予定の番組情報を返却する。
+本日とは、日本時間の0時から23時59分までの間のことを指す。
+
https://agqr.sun-yryr.com/api/today
http://localhost:8080/api/today
Response samples
Content typeapplication/json
[{"title": "string",
"ft": "2019-08-24T14:15:22Z",
"to": "2019-08-24T14:15:22Z",
"pfm": "string",
"dur": 0,
"isBroadcast": true,
"isRepeat": true
}
]
https://agqr.sun-yryr.com/api/now
http://localhost:8080/api/now
Response samples
Content typeapplication/json
[{"title": "string",
"ft": "2019-08-24T14:15:22Z",
"to": "2019-08-24T14:15:22Z",
"pfm": "string",
"dur": 0,
"isBroadcast": true,
"isRepeat": true
}
]
+
+
+
+
diff --git a/Sources/App/Commands/ImportWeeklyPGCommand.swift b/Sources/App/Commands/ImportWeeklyPGCommand.swift
new file mode 100644
index 0000000..3e1a0db
--- /dev/null
+++ b/Sources/App/Commands/ImportWeeklyPGCommand.swift
@@ -0,0 +1,49 @@
+import Fluent
+import Vapor
+
+struct ImportWeeklyPGCommand: Command {
+ let parser: ProgramGuideParsing
+ let repository: ProgramGuideSaving
+ let client = DownloadAgqrProgramGuide()
+
+ struct Signature: CommandSignature {}
+
+ var help: String = "Import weekly program guides into db"
+
+ func run(using context: CommandContext, signature: Signature) throws {
+ context.console.info("Start Process")
+ defer {
+ context.console.info("End Process")
+ }
+
+ let promise = context.application.eventLoopGroup.next().makePromise(of: Void.self)
+ promise.completeWithTask {
+ await self.asyncRun(using: context, signature: signature)
+ }
+
+ try promise.futureResult.wait()
+ }
+
+ func asyncRun(using context: CommandContext, signature: Signature) async {
+ let responses = await client.fetchWeekly(app: context.application)
+ for response in responses {
+ guard let response = response else {
+ context.console.error("NotFound program data")
+ continue
+ }
+ do {
+ let programGuide = try self.parser.parse(response)
+ guard programGuide.count > 0 else {
+ context.console.error("parsed programs length is 0")
+ continue
+ }
+ await self.repository.save(programGuide, app: context.application)
+ context.console.info("success: \(programGuide[0].program.startDatetime)")
+ } catch let error as AgqrParseError {
+ context.console.error(.init(stringLiteral: error.message))
+ } catch {
+ context.console.error(.init(stringLiteral: error.localizedDescription))
+ }
+ }
+ }
+}
diff --git a/Sources/App/Commands/ScrapingAgqrCommand.swift b/Sources/App/Commands/ScrapingAgqrCommand.swift
index 3018e98..3d4d4db 100644
--- a/Sources/App/Commands/ScrapingAgqrCommand.swift
+++ b/Sources/App/Commands/ScrapingAgqrCommand.swift
@@ -7,8 +7,8 @@ struct ScrapingAgqr: Command {
let client = DownloadAgqrProgramGuide()
struct Signature: CommandSignature {
- @Option(name: "url")
- var url: String?
+ @Argument(name: "url")
+ var url: String
}
var help: String = "Download program guide and parse to json."
@@ -18,23 +18,27 @@ struct ScrapingAgqr: Command {
defer {
context.console.info("End Process")
}
- let future = client.execute(app: context.application, url: signature.url)
- .unwrap(or: fatalError("htmlデータの取得に失敗しました"))
- .flatMap { res -> EventLoopFuture in
- do {
- let programGuide = try self.parser.parse(res)
- context.console.info(programGuide.map { element in element.program.startDatetime.toString() }.joined(separator: ","))
- return context.application.eventLoopGroup.future()
- } catch {
- return context.application.eventLoopGroup.future(error: error)
- }
- }
+ let promise = context.application.eventLoopGroup.next().makePromise(of: Void.self)
+ promise.completeWithTask {
+ await self.asyncRun(using: context, signature: signature)
+ }
+
+ try promise.futureResult.wait()
+ }
+
+ func asyncRun(using context: CommandContext, signature: Signature) async {
+ let response = await client.execute(app: context.application, url: signature.url)
+ guard let response = response else {
+ context.console.error("htmlデータの取得に失敗しました")
+ return
+ }
do {
- try future.wait()
+ let programGuide = try parser.parse(response)
+ context.console.info(programGuide.map { element in element.program.startDatetime.toString() }.joined(separator: "\n"))
+ await repository.save(programGuide, app: context.application)
} catch {
- print("Batch failure")
- print(error.localizedDescription)
+ context.console.error(.init(stringLiteral: error.localizedDescription))
}
}
}
diff --git a/Sources/App/Jobs/ImportProgramGuideJob.swift b/Sources/App/Jobs/ImportProgramGuideJob.swift
deleted file mode 100644
index e45f000..0000000
--- a/Sources/App/Jobs/ImportProgramGuideJob.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-import Foundation
-import Queues
-import Vapor
-
-struct ImportProgramGuideJob: ScheduledJob {
- let parser: ProgramGuideParsing
- let repository: ProgramGuideSaving
- let client = DownloadAgqrProgramGuide()
-
- func run(context: QueueContext) -> EventLoopFuture {
- // TODO: ログの生成場所・時間
- context.logger.info("Start Scraping Process")
- defer {
- context.logger.info("End Scraping Process")
- }
-
- return client.fetchWeekly(app: context.application).flatMap { responses -> EventLoopFuture in
- responses
- .map { response -> EventLoopFuture in
- guard let response = response else {
- return context.application.eventLoopGroup.future(error: "番組表データがありませんでした。")
- }
- do {
- let programGuide = try self.parser.parse(response)
- return self.repository.save(programGuide, app: context.application)
- } catch let error as AgqrParseError {
- context.logger.error(.init(stringLiteral: error.message))
- return context.application.eventLoopGroup.future(error: error)
- } catch {
- return context.application.eventLoopGroup.future(error: error)
- }
- }
- .flatten(on: context.application.eventLoopGroup.next())
- }
- }
-}
diff --git a/Sources/App/Repositories/ProgramGuideRepository.swift b/Sources/App/Repositories/ProgramGuideRepository.swift
index 660bfdf..543cafb 100644
--- a/Sources/App/Repositories/ProgramGuideRepository.swift
+++ b/Sources/App/Repositories/ProgramGuideRepository.swift
@@ -2,67 +2,61 @@ import Fluent
import Vapor
protocol ProgramGuideSaving {
- func save(_ items: [ProgramGuide], app: Application) -> EventLoopFuture
+ func save(_ items: [ProgramGuide], app: Application) async
}
struct ProgramGuideRepository: ProgramGuideSaving {
- func save(_ items: [ProgramGuide], app: Application) -> EventLoopFuture {
- // 与えられたProgramGuideからProgramとPersonalitiesをDBに保存し、リレーションを貼る
- let res = items.map { item -> EventLoopFuture in
- // programを取得し、存在する場合は更新する
- let program = upsertProgram(item.program, app.db)
- let personalities = item.personalities.map { upsertPersonality($0, app.db) }
- // 両方の成功時にリレーションを貼る
- return personalities.map {
- $0.flatMap { personality -> EventLoopFuture in
- program.flatMap { program -> EventLoopFuture in
- program.$personalities.isAttached(to: personality, on: app.db).flatMap {
- isAttach -> EventLoopFuture in
- if !isAttach {
- return program.$personalities.attach(personality, on: app.db)
- }
- return app.eventLoopGroup.future()
- }
+ func save(_ items: [ProgramGuide], app: Application) async {
+ for programGuide in items {
+ do {
+ let insertedProgram = try await upsertProgram(programGuide.program, app.db)
+ var insertedPersonalities: [Personality] = []
+ for personality in programGuide.personalities {
+ let p = try await upsertPersonality(personality, app.db)
+ insertedPersonalities.append(p)
+ }
+ for personality in insertedPersonalities {
+ let isAttached = try? await insertedProgram.$personalities.isAttached(
+ to: personality, on: app.db)
+ if isAttached == false {
+ try await insertedProgram.$personalities.attach(personality, on: app.db)
}
}
- }
- .flatten(on: app.eventLoopGroup.next())
- .flatMapError { error in
+ } catch {
+ print("error")
print(error.localizedDescription)
- return app.eventLoopGroup.future(error: error)
}
}
- return res.flatten(on: app.eventLoopGroup.next())
}
/// 開始時間と終了時間を基準に(unique)insert or updateを行う.
- func upsertProgram(_ program: Program, _ db: Database) -> EventLoopFuture {
- return
- Program
- .query(on: db)
+ func upsertProgram(_ program: Program, _ db: Database) async throws -> Program {
+ let targetProgram = try? await Program.query(on: db)
.filter(\.$startDatetime == program.startDatetime)
.filter(\.$endDatetime == program.endDatetime)
.first()
- .flatMap { dbProgram -> EventLoopFuture in
- program.id = dbProgram?.id
- // idを挿入するだけだとうまくupdateできなかったため、判定要素を上書きする
- program._$id.exists = dbProgram?.id != nil
- return program.save(on: db).transform(to: program)
- }
+
+ program.id = targetProgram?.id
+ // idを挿入するだけだとうまくupdateできなかったため、判定要素を上書きする
+ program._$id.exists = targetProgram != nil
+
+ try await program.save(on: db)
+ return program
}
/// 名前を基準に(unique)insert or updateを行う.
- func upsertPersonality(_ personality: Personality, _ db: Database) -> EventLoopFuture {
- return
- Personality
+ func upsertPersonality(_ personality: Personality, _ db: Database) async throws -> Personality {
+ let targetPersonality =
+ try? await Personality
.query(on: db)
.filter(\.$name == personality.name)
.first()
- .flatMap { dbPersonality -> EventLoopFuture in
- personality.id = dbPersonality?.id
- // idを挿入するだけだとうまくupdateできなかったため、判定要素を上書きする
- personality._$id.exists = dbPersonality?.id != nil
- return personality.save(on: db).transform(to: personality)
- }
+
+ personality.id = targetPersonality?.id
+ // idを挿入するだけだとうまくupdateできなかったため、判定要素を上書きする
+ personality._$id.exists = targetPersonality != nil
+
+ try await personality.save(on: db)
+ return personality
}
}
diff --git a/Sources/App/Services/DownloadAgqrProgramGuide.swift b/Sources/App/Services/DownloadAgqrProgramGuide.swift
index 87403d5..70d52cd 100644
--- a/Sources/App/Services/DownloadAgqrProgramGuide.swift
+++ b/Sources/App/Services/DownloadAgqrProgramGuide.swift
@@ -4,13 +4,11 @@ struct DownloadAgqrProgramGuide {
// swift-format-ignore
static let AGQR_URL = "https://www.joqr.co.jp/qr/agdailyprogram/"
- func fetchToday(app: Application) -> EventLoopFuture {
- return app.client.get(URI(string: Self.AGQR_URL)).map { res -> Data? in
- return res.body.flatMap { $0.getData(at: 0, length: $0.writerIndex) }
- }
+ func fetchToday(app: Application) async -> Data? {
+ return await self.execute(app: app, url: Self.AGQR_URL)
}
- func fetchWeekly(app: Application) -> EventLoopFuture<[Data?]> {
+ func fetchWeekly(app: Application) async -> [Data?] {
// query date format=yyyyMMdd
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
@@ -21,16 +19,30 @@ struct DownloadAgqrProgramGuide {
return formatter.string(from: calculateDate)
}.map { "\(Self.AGQR_URL)?date=\($0)" }
- return urls.map { url -> EventLoopFuture in
- app.client.get(URI(string: url)).map { res -> Data? in
- return res.body.flatMap { $0.getData(at: 0, length: $0.writerIndex) }
+ var responses: [Data?] = []
+ await withTaskGroup(of: Data?.self) { group in
+ for url in urls {
+ group.addTask {
+ return await self.execute(app: app, url: url)
+ }
+ }
+
+ for await response in group {
+ responses.append(response)
}
- }.flatten(on: app.eventLoopGroup.next())
+ }
+ return responses
}
- func execute(app: Application, url: String?) -> EventLoopFuture {
- return app.client.get(URI(string: url ?? Self.AGQR_URL)).map { res in
- return res.body.flatMap { $0.getData(at: 0, length: $0.writerIndex) }
+ func execute(app: Application, url: String) async -> Data? {
+ do {
+ let response = try await app.client.get(URI(string: url)).get()
+ return response.body.flatMap { buffer in
+ buffer.getData(at: 0, length: buffer.writerIndex)
+ }
+ } catch {
+ print(error.localizedDescription)
+ return nil
}
}
}
diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift
index a37b1c5..c2da4b1 100644
--- a/Sources/App/configure.swift
+++ b/Sources/App/configure.swift
@@ -1,24 +1,22 @@
import Fluent
-import FluentMySQLDriver
+import FluentPostgresDriver
import Leaf
-import Queues
-import QueuesRedisDriver
import Vapor
// configures your application
public func configure(_ app: Application) throws {
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
-
app.databases.use(
- .mysql(
+ .postgres(
hostname: Environment.get("DATABASE_HOST") ?? "localhost",
- port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? MySQLConfiguration.ianaPortNumber,
+ port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? PostgresConfiguration.ianaPortNumber,
username: Environment.get("DATABASE_USERNAME") ?? "vapor_username",
password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password",
- database: Environment.get("DATABASE_NAME") ?? "vapor_database",
- tlsConfiguration: .forClient(certificateVerification: .none)
- ), as: .mysql)
+ database: Environment.get("DATABASE_NAME") ?? "vapor_database"
+ ),
+ as: .psql
+ )
app.migrations.add(CreateProgram())
app.migrations.add(CreatePersonality())
@@ -26,30 +24,28 @@ public func configure(_ app: Application) throws {
app.views.use(.leaf)
+ let pgParser = DailyProgramGuideParser()
+ let pgRepository = ProgramGuideRepository()
app.commands.use(
- ScrapingAgqr(parser: DailyProgramGuideParser(), repository: ProgramGuideRepository()), as: "scraping")
-
- try app.queues.use(.redis(url: Environment.get("REDIS_URL") ?? "redis://127.0.0.1:6379"))
-
- app.queues.schedule(
- ImportProgramGuideJob(parser: DailyProgramGuideParser(), repository: ProgramGuideRepository())
+ ScrapingAgqr(parser: pgParser, repository: pgRepository), as: "scraping")
+ app.commands.use(
+ ImportWeeklyPGCommand(parser: pgParser, repository: pgRepository), as: "import:weekly"
)
- .daily()
- .at(7, 0) // 07:00 am
-
- // try app.queues.startInProcessJobs(on: .default)
- try app.queues.startScheduledJobs()
// register middleware
+ app.middleware.use(newCORSMiddleware())
+
+ // register routes
+ try routes(app)
+}
+
+func newCORSMiddleware() -> CORSMiddleware {
let corsConfiguration = CORSMiddleware.Configuration(
allowedOrigin: .all,
allowedMethods: [.GET, .OPTIONS, .HEAD],
allowedHeaders: [
- .accept, .contentType, .origin, .xRequestedWith, .userAgent, .accessControlAllowOrigin
+ .accept, .contentType, .origin, .xRequestedWith, .userAgent, .accessControlAllowOrigin,
]
)
- app.middleware.use(CORSMiddleware(configuration: corsConfiguration))
-
- // register routes
- try routes(app)
+ return CORSMiddleware(configuration: corsConfiguration)
}
diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift
index f76ac49..c265835 100644
--- a/Sources/App/routes.swift
+++ b/Sources/App/routes.swift
@@ -3,11 +3,11 @@ import Vapor
func routes(_ app: Application) throws {
app.get { req in
- return req.view.render("index", ["title": "Hello Vapor!"])
+ return req.view.render("redoc-static.html")
}
- app.get("hello") { _ -> String in
- return "Hello, world!"
+ app.get("health") { _ -> Response in
+ return Response(status: .ok)
}
try app.group("api") { builder in
diff --git a/Tests/AppTests/AppTests.swift b/Tests/AppTests/AppTests.swift
index 1d4a8d0..11597a8 100644
--- a/Tests/AppTests/AppTests.swift
+++ b/Tests/AppTests/AppTests.swift
@@ -2,10 +2,9 @@
import XCTVapor
final class AppTests: ControllerBaseTestCase {
- func testHelloWorld() throws {
- try app.test(.GET, "hello", afterResponse: { res in
+ func testHealth() throws {
+ try app.test(.GET, "health", afterResponse: { res in
XCTAssertEqual(res.status, .ok)
- XCTAssertEqual(res.body.string, "Hello, world!")
})
}
}
diff --git a/docker-compose.yml b/compose.yaml
similarity index 78%
rename from docker-compose.yml
rename to compose.yaml
index 03070fa..6034180 100644
--- a/docker-compose.yml
+++ b/compose.yaml
@@ -14,8 +14,6 @@
# Run migrations: docker-compose run migrate
# Stop all: docker-compose down (add -v to wipe db)
#
-version: '3.7'
-
volumes:
db_data:
@@ -40,6 +38,7 @@ services:
command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
dev:
image: agqr-program-guide:latest
+ platform: linux/amd64
build:
context: .
target: develop
@@ -49,6 +48,7 @@ services:
- db
commands:
image: agqr-program-guide:latest
+ platform: linux/amd64
build:
context: .
environment:
@@ -57,19 +57,18 @@ services:
- db
command: ["--help"]
db:
- image: mysql:8.0
+ image: postgres:15.5
volumes:
- - db_data:/var/lib/mysql
- - ./docker/mysql:/docker-entrypoint-initdb.d
+ - db_data:/var/lib/postgresql
+ - ./docker/db:/docker-entrypoint-initdb.d
environment:
- MYSQL_USER: vapor_username
- MYSQL_PASSWORD: vapor_password
- MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
+ POSTGRES_USER: vapor_username
+ POSTGRES_PASSWORD: vapor_password
ports:
- - '3306:3306'
- redis:
- image: redis:latest
- ports:
- - '6379:6379'
- volumes:
- - ./data/redis:/data
+ - '5432:5432'
+ healthcheck:
+ test: ["CMD", "pg_isready", "-U", "vapor_username"]
+ interval: 10s
+ timeout: 30s
+ retries: 5
+ start_period: 30s
diff --git a/docker/db/01_create_databases.sql b/docker/db/01_create_databases.sql
new file mode 100755
index 0000000..49f3ccc
--- /dev/null
+++ b/docker/db/01_create_databases.sql
@@ -0,0 +1,4 @@
+CREATE DATABASE agqr_program_guide;
+CREATE DATABASE test_agqr_program_guide;
+GRANT ALL PRIVILEGES ON DATABASE agqr_program_guide TO vapor_username;
+GRANT ALL PRIVILEGES ON DATABASE test_agqr_program_guide TO vapor_username;
diff --git a/docker/mysql/01_create_databases.sql b/docker/mysql/01_create_databases.sql
deleted file mode 100755
index 45a52b8..0000000
--- a/docker/mysql/01_create_databases.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-CREATE DATABASE IF NOT EXISTS `agqr_program_guide`;
-GRANT ALL ON `agqr_program_guide`.* TO 'vapor_username'@'%';
-CREATE DATABASE IF NOT EXISTS `test_agqr_program_guide`;
-GRANT ALL ON `test_agqr_program_guide`.* TO 'vapor_username'@'%';
diff --git a/reference/agqr-radio-program-guide-api.v2.yaml b/reference/agqr-radio-program-guide-api.v2.yaml
index a1ca023..6a236ba 100644
--- a/reference/agqr-radio-program-guide-api.v2.yaml
+++ b/reference/agqr-radio-program-guide-api.v2.yaml
@@ -2,88 +2,26 @@ openapi: 3.0.0
info:
title: agqr-radio-program-guide-api
version: '1.0'
- contact:
- name: sun-yryr(t_minagawa)
- url: 'https://twitter.com/taittide'
- email: taittide@gmail.com
+ description: |-
+ **[非公式] 超A&G+ 番組表 API**
+ データ更新頻度: 1日ごと(JST 07:00 ごろ)
+
+ 何かバグ等ありましたら、Issue を立てていただくか下記の連絡先までご連絡ください。
+ Twitter(X): [@sun_yryr](https://twitter.com/sun_yryr)
+ GitHub: https://github.com/sun-yryr/agqr-program-guide
servers:
+ - url: 'https://agqr.sun-yryr.com/api'
+ description: production
- url: 'http://localhost:8080/api'
description: local
- - url: 'https://agqr.sun-yryr.com/api'
- description: prod
+tags:
+ - name: v1
+ description: 'v1 API'
paths:
- /v2/programs/weekly:
- get:
- summary: '[未実装] 週間番組表を取得'
- tags: []
- responses:
- '200':
- description: OK(検索結果がない場合は空配列になります)
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/program'
- operationId: get-v2-programs-weekly
- description: 条件に合った番組情報を返却する。
- parameters:
- - schema:
- type: string
- in: query
- name: q
- description: '検索ワード(title,personality,infoの全てを対象)'
- - schema:
- type: string
- example: 'audio,movie,repeat'
- default: 'audio,movie'
- in: query
- name: include_program_type
- description: |-
- 検索結果に含める番組タイプ
- [,]区切りで記述
- movie: 動画付きのみ
- audio: 音声のみ
- repeat: 再放送を含める
- parameters: []
- /v2/programs/daily:
- get:
- summary: '[未実装] 日間番組表を取得'
- tags: []
- responses:
- '200':
- description: OK(検索結果がない場合は空配列になります)
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/program'
- operationId: get-v2-programs-daily
- description: 条件に合った番組情報を返却する
- parameters:
- - schema:
- type: string
- in: query
- name: q
- description: '検索ワード(title,personality,infoの全てを対象)'
- - schema:
- type: string
- example: 'audio,movie,repeat'
- default: 'audio,movie'
- in: query
- name: include_program_type
- description: |-
- 検索結果に含める番組タイプ
- [,]区切りで記述
- movie: 動画付きのみ
- audio: 音声のみ
- repeat: 再放送を含める
- parameters: []
/all:
get:
summary: 週刊番組表を取得
- tags: []
+ tags: [v1]
responses:
'200':
description: OK
@@ -95,12 +33,12 @@ paths:
$ref: '#/components/schemas/old-program'
operationId: get-all
description: |-
- 1週間分全ての番組情報を返却します
- そのうち削除予定
+ 1週間分全ての番組情報を返却する。
+ 実行日の日本時間の0時から7日後の23時59分までの間の番組情報を返却する。
/today:
get:
summary: 日間番組表を取得
- tags: []
+ tags: [v1]
responses:
'200':
description: OK
@@ -112,12 +50,12 @@ paths:
$ref: '#/components/schemas/old-program'
operationId: get-today
description: |-
- 本日放送予定の番組情報を返却します
- そのうち削除予定
+ 本日放送予定の番組情報を返却する。
+ 本日とは、日本時間の0時から23時59分までの間のことを指す。
/now:
get:
summary: 現在の番組情報を返却
- tags: []
+ tags: [v1]
responses:
'200':
description: OK
@@ -128,37 +66,7 @@ paths:
items:
$ref: '#/components/schemas/old-program'
operationId: get-now
- description: |-
- 現在放送中の番組情報を返却します
- そのうち削除予定
- /v2/programs/now:
- get:
- summary: '[未実装] 現在の番組情報を返却'
- tags: []
- responses:
- '200':
- description: OK
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/program'
- '404':
- description: 現在放送中の番組がありません
- operationId: get-v2-programs-now
- description: 現在放送中の番組情報を返却します
- '/v2/programs/{programId}':
- parameters:
- - schema:
- type: string
- name: programId
- in: path
- required: true
- get:
- summary: '[未実装] 番組詳細を取得する'
- tags: []
- responses: {}
- operationId: get-v2-programs-programId
- description: ''
+ description: 現在放送中の番組情報を返却する
components:
schemas:
program:
diff --git a/tools/Package.resolved b/tools/Package.resolved
index d6a8022..abc38d1 100644
--- a/tools/Package.resolved
+++ b/tools/Package.resolved
@@ -6,16 +6,16 @@
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
"state": {
"branch": null,
- "revision": "83b23d940471b313427da226196661856f6ba3e0",
- "version": "0.4.4"
+ "revision": "e1465042f195f374b94f915ba8ca49de24300a0d",
+ "version": "1.0.2"
}
},
{
"package": "swift-format",
"repositoryURL": "https://github.com/apple/swift-format",
"state": {
- "branch": "swift-5.4-branch",
- "revision": "9c15831b798d767c9af0927a931de5d557004936",
+ "branch": "swift-5.5-branch",
+ "revision": "f872223e16742fd97fabd319fbf4a939230cc796",
"version": null
}
},
@@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/apple/swift-syntax",
"state": {
"branch": null,
- "revision": "2fff9fc25cdc059379b6bd309377cfab45d8520c",
- "version": "0.50400.0"
+ "revision": "75e60475d9d8fd5bbc16a12e0eaa2cb01b0c322e",
+ "version": "0.50500.0"
}
}
]
diff --git a/tools/Package.swift b/tools/Package.swift
index 59a59d2..3cfdbc6 100644
--- a/tools/Package.swift
+++ b/tools/Package.swift
@@ -1,10 +1,10 @@
-// swift-tools-version:5.2
+// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "Tools",
dependencies: [
- .package(url: "https://github.com/apple/swift-format", .branch("swift-5.4-branch")),
+ .package(url: "https://github.com/apple/swift-format", .branch("swift-5.5-branch")),
// .package(url: "https://github.com/realm/SwiftLint.git", .upToNextMajor(from: "0.43.1"))
]
)