popeyelau / wiki

📒Wiki for many useful notes, source, commands and snippets.
2 stars 0 forks source link

SwiftCLI #19

Open popeyelau opened 5 years ago

popeyelau commented 5 years ago

Package.swift

import PackageDescription

let package = Package(
    name: "dogetv-cli",
    dependencies: [
        .package(url: "https://github.com/jakeheis/SwiftCLI", from: "5.2.2"),
        .package(url: "https://github.com/onevcat/Rainbow", from: "3.0.0")
    ],
    targets: [
        .target(
            name: "dogetv-cli",
            dependencies: ["SwiftCLI", "Rainbow"]),
    ]
)

main.swift

#if os(OSX)
import Darwin
#else
import Glibc
#endif
import Foundation
import SwiftCLI
import Rainbow

let HOST = "https://tv.popeye.vip"

func makeRequest<T: Decodable> (url: URL, type: T.Type) -> T? {
    let sema: DispatchSemaphore = DispatchSemaphore(value: 0)
    var result: T? = nil
    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let data = data {
            result = try? JSONDecoder().decode(T.self, from: data)
        }
        sema.signal()
    }
    task.resume()
    sema.wait()
    return result
}

func getBody(from url: URL) -> String? {
    let sema: DispatchSemaphore = DispatchSemaphore(value: 0)
    var result: String? = nil
    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let data = data {
            result = String.init(data: data, encoding: .utf8)
        }
        sema.signal()
    }
    task.resume()
    sema.wait()
    return result
}

class SearchCommand: Command {
    let name: String = "search"
    let shortDescription: String = "搜索电影/电视剧/综艺/影人"
    let keyword = Parameter()

    func execute() throws {
        let url = URL(string: "\(HOST)/pumpkin/search/\(keyword.value.encodedURI)")!;
        let result = makeRequest(url: url, type: Response<[Video]>.self)

        guard let videos = result?.data, !videos.isEmpty else {
            stderr <<< "未找到相关资源".yellow
            return
        }

        let output = videos.enumerated().reduce("") { (result, item) -> String in
            return result  + "\n[\(String(item.offset).blue.underline)] \(item.element.name)"
        }

        stdout <<< output

        let msg = "区间 0 ~ \(videos.count - 1)".red
        let index = Input.readInt(prompt: "Input index:".lightCyan, validation: [.greaterThan(-1, message: msg), .lessThan(videos.count, message: msg)])
        let video = videos[index]

        if !video.cover.contains("vcinema") {
            _ = cli.go(with: ["blueray-episodes", video.id])
            return
        }

        _ = cli.go(with: ["detail", video.id])
    }
}

class VideoDetailCommand: Command {
    let name: String = "detail"
    let shortDescription: String = "视频信息"
    let id = Parameter()

    func execute() throws {

        let url = URL(string: "\(HOST)/pumpkin/video/\(id.value)")!
        let result = makeRequest(url: url, type: Response<VideoDetail>.self)
        guard let videoDetail = result?.data else {
            stderr <<< "未找到相关资源".yellow
            return
        }

        guard let seasons = videoDetail.seasons, !seasons.isEmpty else{
            _ = cli.go(with: ["episodes", id.value])
            return
        }

        let output = seasons.enumerated().reduce("") { (result, item) -> String in
            return result + (item.offset % 4 == 0 ? "\n" : "") + "[\(String(item.offset).blue.underline)] \(item.element.name)\t"
        }

        stdout <<< output

        let msg = "有效区间 0 ~ \(seasons.count - 1)".red
        let index = Input.readInt(prompt: "Input index:".lightCyan, validation: [.greaterThan(-1, message: msg), .lessThan(seasons.count, message: msg)])
        let seasonId = seasons[index].id
        _ = cli.go(with: ["season-episodes", id.value, seasonId])
    }
}

class VideoEpisodesCommand: Command {
    let name: String = "episodes"
    let shortDescription: String = "播放列表"
    let id = Parameter()

    func execute() throws {
        let url = URL(string: "\(HOST)/pumpkin/video/\(id.value)/stream")!
        let result = makeRequest(url: url, type: Response<[Episode]>.self)

        guard let episodes = result?.data, !episodes.isEmpty else {
            stderr <<< "未找到相关资源".yellow
            return
        }

        let output = episodes.enumerated().reduce("") { (result, item) -> String in
            return result + (item.offset % 4 == 0 ? "\n" : "") + "[\(String(item.offset).blue.underline)] \(item.element.title)\t"
        }

        stdout <<< output

        let msg = "有效区间 0 ~ \(episodes.count - 1)".red
        let index = Input.readInt(prompt: "Input index:".lightCyan, validation: [.greaterThan(-1, message: msg), .lessThan(episodes.count, message: msg)])

        let streamUrl = episodes[index].url

        do {
            try run("open", "iina://weblink?url=\(streamUrl.encodedURI)")
        } catch {
            stderr <<< error.localizedDescription.red
        }

    }
}

class SeasonEpisodesCommand: Command {
    let name: String = "season-episodes"
    let shortDescription: String = "分季播放列表"
    let id = Parameter()
    let seasonId = Parameter()

    func execute() throws {
        let url = URL(string: "\(HOST)/pumpkin/video/\(id.value)?sid=\(seasonId.value)")!
        let result = makeRequest(url: url, type: Response<VideoDetail>.self)

        guard let videoDetail = result?.data, let seasons = videoDetail.seasons, !seasons.isEmpty else {
            stderr <<< "未找到相关资源".yellow
            return
        }

        let season = seasons.first { $0.episodes?.isEmpty == false }
        guard let episodes = season?.episodes, !episodes.isEmpty else {
            stderr <<< "未找到相关资源".yellow
            return
        }

        let output = episodes.enumerated().reduce("") { (result, item) -> String in
            return result + (item.offset % 4 == 0 ? "\n" : "") + "[\(String(item.offset).blue.underline)] \(item.element.title)\t"
        }

        stdout <<< output

        let msg = "有效区间 0 ~ \(episodes.count - 1)".red
        let index = Input.readInt(prompt: "Input index:".lightCyan.underline, validation: [.greaterThan(-1, message: msg), .lessThan(episodes.count, message: msg)])

        guard let episodeId = episodes[index].id else {
            stderr <<< "未找到相关资源".yellow
            return
        }
        _ = cli.go(with: ["episodes", episodeId])
    }
}

class BluerayEpisodesCommand: Command {
    let name: String = "blueray-episodes"
    let shortDescription: String = "高清资源播放列表"
    let id = Parameter()

    func execute() throws {
        let url = URL(string: "\(HOST)/4k/detail/\(id.value)/episodes")!
        let result = makeRequest(url: url, type: Response<[Episode]>.self)

        guard let episodes = result?.data, !episodes.isEmpty else {
            stderr <<< "未找到相关资源".yellow
            return
        }

        let output = episodes.enumerated().reduce("") { (result, item) -> String in
            return result + (item.offset % 4 == 0 ? "\n" : "") + "[\(String(item.offset).blue.underline)] \(item.element.title)\t"
        }

        stdout <<< output

        let msg = "有效区间 0 ~ \(episodes.count - 1)".red
        let index = Input.readInt(prompt: "Input index:".lightCyan, validation: [.greaterThan(-1, message: msg), .lessThan(episodes.count, message: msg)])

        guard let episodeUrl = URL(string: episodes[index].url) else {
            stderr <<< "未找到相关资源".yellow
            return
        }

        guard let body = getBody(from: episodeUrl) else { return }
        guard let regex = try? NSRegularExpression(pattern: "(http:|https:)//(.*).m3u8") else { return }
        guard let matched = regex.firstMatch(in: body, range: NSRange(body.startIndex..., in: body)) else {
            return
        }

        let m3u8 = String(body[Range(matched.range, in: body)!])

        do {
            try run("open", "iina://weblink?url=\(m3u8.encodedURI)")
        } catch {
            stderr <<< error.localizedDescription.yellow
        }

    }
}

let cli = CLI(name: "dogetv-cli", version: "1.0.0", description: "搜索电影/电视剧/综艺/影人", commands: [SearchCommand(), VideoDetailCommand(), VideoEpisodesCommand(), SeasonEpisodesCommand(), BluerayEpisodesCommand()])
_ = cli.go()

extensions.swift

#if os(OSX)
    import Darwin
#else
    import Glibc
#endif
import Foundation

extension String {
    public var encodedURI: String {
        var characterSet = CharacterSet.alphanumerics
        characterSet.insert(charactersIn: "-_.!~*'()")
        return self.addingPercentEncoding(withAllowedCharacters: characterSet) ?? self
    }
}

models.swift

#if os(OSX)
    import Darwin
#else
    import Glibc
#endif
import Foundation

public struct Response<T: Decodable>: Decodable {
    public let code: Int
    public let msg: String
    public let data: T
}

public struct IPTV: Decodable {
    public let id: String
    public let category: String
}

public struct Video: Decodable {
    public let id: String
    public let name: String
    public let cover: String
}

public struct Season: Decodable {
    public let id: String
    public let name: String
    public let episodes: [Episode]?
}

public struct Episode: Decodable {
    public let id: String?
    public let url: String
    public let title: String
}

public struct VideoDetail: Decodable {
    public let info: Video
    public let seasons: [Season]?
}

build

$swift build -c release