#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]?
}
Package.swift
main.swift
extensions.swift
models.swift
build
$
swift build -c release