├── README.md ├── TestVOD.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── ronnie.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── TestVOD.xcscheme └── xcuserdata │ └── qoli.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── TestVOD ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── String.swift ├── TestVOD.entitlements ├── TestVODApp.swift ├── URL.swift └── api.swift └── assets ├── SCR-20230207-n48.png ├── SCR-20230207-ncx.png └── SCR-20230207-nfi.png /README.md: -------------------------------------------------------------------------------- 1 | # TestVOD 2 | 一個基於正則測試 VOD 可用性的 App,SwiftUI 編寫。 3 | 4 | ![SCR-20230207-ncx](assets/SCR-20230207-ncx.png) 5 | 6 | ![SCR-20230207-nfi](assets/SCR-20230207-nfi.png) 7 | 8 | ## 運作方法 9 | 10 | 匹配正則:用於匹配 URL 的格式; 11 | 12 | 開始測試:使用搜索功能去測試每一個成功匹配的 VOD。 13 | -------------------------------------------------------------------------------- /TestVOD.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F98F4D972989B4FE00D6ABE8 /* TestVODApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98F4D962989B4FE00D6ABE8 /* TestVODApp.swift */; }; 11 | F98F4D992989B4FE00D6ABE8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98F4D982989B4FE00D6ABE8 /* ContentView.swift */; }; 12 | F98F4D9B2989B50100D6ABE8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F98F4D9A2989B50100D6ABE8 /* Assets.xcassets */; }; 13 | F98F4D9F2989B50100D6ABE8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F98F4D9E2989B50100D6ABE8 /* Preview Assets.xcassets */; }; 14 | F98F4DA62989B59000D6ABE8 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98F4DA52989B59000D6ABE8 /* String.swift */; }; 15 | F98F4DA82989B62D00D6ABE8 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98F4DA72989B62D00D6ABE8 /* URL.swift */; }; 16 | F98F4DAA2989B63A00D6ABE8 /* api.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98F4DA92989B63A00D6ABE8 /* api.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | F98F4D932989B4FE00D6ABE8 /* TestVOD.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestVOD.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | F98F4D962989B4FE00D6ABE8 /* TestVODApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestVODApp.swift; sourceTree = ""; }; 22 | F98F4D982989B4FE00D6ABE8 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 23 | F98F4D9A2989B50100D6ABE8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | F98F4D9C2989B50100D6ABE8 /* TestVOD.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestVOD.entitlements; sourceTree = ""; }; 25 | F98F4D9E2989B50100D6ABE8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 26 | F98F4DA52989B59000D6ABE8 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 27 | F98F4DA72989B62D00D6ABE8 /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 28 | F98F4DA92989B63A00D6ABE8 /* api.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = api.swift; sourceTree = ""; }; 29 | F98F4DAB2989B7E000D6ABE8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | F98F4D902989B4FE00D6ABE8 /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | ); 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXFrameworksBuildPhase section */ 41 | 42 | /* Begin PBXGroup section */ 43 | F98F4D8A2989B4FE00D6ABE8 = { 44 | isa = PBXGroup; 45 | children = ( 46 | F98F4D952989B4FE00D6ABE8 /* TestVOD */, 47 | F98F4D942989B4FE00D6ABE8 /* Products */, 48 | ); 49 | sourceTree = ""; 50 | }; 51 | F98F4D942989B4FE00D6ABE8 /* Products */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | F98F4D932989B4FE00D6ABE8 /* TestVOD.app */, 55 | ); 56 | name = Products; 57 | sourceTree = ""; 58 | }; 59 | F98F4D952989B4FE00D6ABE8 /* TestVOD */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | F98F4DAB2989B7E000D6ABE8 /* Info.plist */, 63 | F98F4DA92989B63A00D6ABE8 /* api.swift */, 64 | F98F4DA72989B62D00D6ABE8 /* URL.swift */, 65 | F98F4DA52989B59000D6ABE8 /* String.swift */, 66 | F98F4D962989B4FE00D6ABE8 /* TestVODApp.swift */, 67 | F98F4D982989B4FE00D6ABE8 /* ContentView.swift */, 68 | F98F4D9A2989B50100D6ABE8 /* Assets.xcassets */, 69 | F98F4D9C2989B50100D6ABE8 /* TestVOD.entitlements */, 70 | F98F4D9D2989B50100D6ABE8 /* Preview Content */, 71 | ); 72 | path = TestVOD; 73 | sourceTree = ""; 74 | }; 75 | F98F4D9D2989B50100D6ABE8 /* Preview Content */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | F98F4D9E2989B50100D6ABE8 /* Preview Assets.xcassets */, 79 | ); 80 | path = "Preview Content"; 81 | sourceTree = ""; 82 | }; 83 | /* End PBXGroup section */ 84 | 85 | /* Begin PBXNativeTarget section */ 86 | F98F4D922989B4FE00D6ABE8 /* TestVOD */ = { 87 | isa = PBXNativeTarget; 88 | buildConfigurationList = F98F4DA22989B50100D6ABE8 /* Build configuration list for PBXNativeTarget "TestVOD" */; 89 | buildPhases = ( 90 | F98F4D8F2989B4FE00D6ABE8 /* Sources */, 91 | F98F4D902989B4FE00D6ABE8 /* Frameworks */, 92 | F98F4D912989B4FE00D6ABE8 /* Resources */, 93 | ); 94 | buildRules = ( 95 | ); 96 | dependencies = ( 97 | ); 98 | name = TestVOD; 99 | productName = TestVOD; 100 | productReference = F98F4D932989B4FE00D6ABE8 /* TestVOD.app */; 101 | productType = "com.apple.product-type.application"; 102 | }; 103 | /* End PBXNativeTarget section */ 104 | 105 | /* Begin PBXProject section */ 106 | F98F4D8B2989B4FE00D6ABE8 /* Project object */ = { 107 | isa = PBXProject; 108 | attributes = { 109 | BuildIndependentTargetsInParallel = 1; 110 | LastSwiftUpdateCheck = 1420; 111 | LastUpgradeCheck = 1420; 112 | TargetAttributes = { 113 | F98F4D922989B4FE00D6ABE8 = { 114 | CreatedOnToolsVersion = 14.2; 115 | }; 116 | }; 117 | }; 118 | buildConfigurationList = F98F4D8E2989B4FE00D6ABE8 /* Build configuration list for PBXProject "TestVOD" */; 119 | compatibilityVersion = "Xcode 14.0"; 120 | developmentRegion = en; 121 | hasScannedForEncodings = 0; 122 | knownRegions = ( 123 | en, 124 | Base, 125 | ); 126 | mainGroup = F98F4D8A2989B4FE00D6ABE8; 127 | productRefGroup = F98F4D942989B4FE00D6ABE8 /* Products */; 128 | projectDirPath = ""; 129 | projectRoot = ""; 130 | targets = ( 131 | F98F4D922989B4FE00D6ABE8 /* TestVOD */, 132 | ); 133 | }; 134 | /* End PBXProject section */ 135 | 136 | /* Begin PBXResourcesBuildPhase section */ 137 | F98F4D912989B4FE00D6ABE8 /* Resources */ = { 138 | isa = PBXResourcesBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | F98F4D9F2989B50100D6ABE8 /* Preview Assets.xcassets in Resources */, 142 | F98F4D9B2989B50100D6ABE8 /* Assets.xcassets in Resources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXResourcesBuildPhase section */ 147 | 148 | /* Begin PBXSourcesBuildPhase section */ 149 | F98F4D8F2989B4FE00D6ABE8 /* Sources */ = { 150 | isa = PBXSourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | F98F4DAA2989B63A00D6ABE8 /* api.swift in Sources */, 154 | F98F4DA62989B59000D6ABE8 /* String.swift in Sources */, 155 | F98F4D992989B4FE00D6ABE8 /* ContentView.swift in Sources */, 156 | F98F4D972989B4FE00D6ABE8 /* TestVODApp.swift in Sources */, 157 | F98F4DA82989B62D00D6ABE8 /* URL.swift in Sources */, 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXSourcesBuildPhase section */ 162 | 163 | /* Begin XCBuildConfiguration section */ 164 | F98F4DA02989B50100D6ABE8 /* Debug */ = { 165 | isa = XCBuildConfiguration; 166 | buildSettings = { 167 | ALWAYS_SEARCH_USER_PATHS = NO; 168 | CLANG_ANALYZER_NONNULL = YES; 169 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 170 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 171 | CLANG_ENABLE_MODULES = YES; 172 | CLANG_ENABLE_OBJC_ARC = YES; 173 | CLANG_ENABLE_OBJC_WEAK = YES; 174 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 175 | CLANG_WARN_BOOL_CONVERSION = YES; 176 | CLANG_WARN_COMMA = YES; 177 | CLANG_WARN_CONSTANT_CONVERSION = YES; 178 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 179 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 180 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 181 | CLANG_WARN_EMPTY_BODY = YES; 182 | CLANG_WARN_ENUM_CONVERSION = YES; 183 | CLANG_WARN_INFINITE_RECURSION = YES; 184 | CLANG_WARN_INT_CONVERSION = YES; 185 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 186 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 187 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 188 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 189 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 190 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 191 | CLANG_WARN_STRICT_PROTOTYPES = YES; 192 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 193 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 194 | CLANG_WARN_UNREACHABLE_CODE = YES; 195 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 196 | COPY_PHASE_STRIP = NO; 197 | DEBUG_INFORMATION_FORMAT = dwarf; 198 | ENABLE_STRICT_OBJC_MSGSEND = YES; 199 | ENABLE_TESTABILITY = YES; 200 | GCC_C_LANGUAGE_STANDARD = gnu11; 201 | GCC_DYNAMIC_NO_PIC = NO; 202 | GCC_NO_COMMON_BLOCKS = YES; 203 | GCC_OPTIMIZATION_LEVEL = 0; 204 | GCC_PREPROCESSOR_DEFINITIONS = ( 205 | "DEBUG=1", 206 | "$(inherited)", 207 | ); 208 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 209 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 210 | GCC_WARN_UNDECLARED_SELECTOR = YES; 211 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 212 | GCC_WARN_UNUSED_FUNCTION = YES; 213 | GCC_WARN_UNUSED_VARIABLE = YES; 214 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 215 | MTL_FAST_MATH = YES; 216 | ONLY_ACTIVE_ARCH = YES; 217 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 218 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 219 | }; 220 | name = Debug; 221 | }; 222 | F98F4DA12989B50100D6ABE8 /* Release */ = { 223 | isa = XCBuildConfiguration; 224 | buildSettings = { 225 | ALWAYS_SEARCH_USER_PATHS = NO; 226 | CLANG_ANALYZER_NONNULL = YES; 227 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 228 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 229 | CLANG_ENABLE_MODULES = YES; 230 | CLANG_ENABLE_OBJC_ARC = YES; 231 | CLANG_ENABLE_OBJC_WEAK = YES; 232 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 233 | CLANG_WARN_BOOL_CONVERSION = YES; 234 | CLANG_WARN_COMMA = YES; 235 | CLANG_WARN_CONSTANT_CONVERSION = YES; 236 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 238 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 239 | CLANG_WARN_EMPTY_BODY = YES; 240 | CLANG_WARN_ENUM_CONVERSION = YES; 241 | CLANG_WARN_INFINITE_RECURSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 244 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 245 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 246 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 247 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 248 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 249 | CLANG_WARN_STRICT_PROTOTYPES = YES; 250 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 251 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 252 | CLANG_WARN_UNREACHABLE_CODE = YES; 253 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 254 | COPY_PHASE_STRIP = NO; 255 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 256 | ENABLE_NS_ASSERTIONS = NO; 257 | ENABLE_STRICT_OBJC_MSGSEND = YES; 258 | GCC_C_LANGUAGE_STANDARD = gnu11; 259 | GCC_NO_COMMON_BLOCKS = YES; 260 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 261 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 262 | GCC_WARN_UNDECLARED_SELECTOR = YES; 263 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 264 | GCC_WARN_UNUSED_FUNCTION = YES; 265 | GCC_WARN_UNUSED_VARIABLE = YES; 266 | MTL_ENABLE_DEBUG_INFO = NO; 267 | MTL_FAST_MATH = YES; 268 | SWIFT_COMPILATION_MODE = wholemodule; 269 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 270 | }; 271 | name = Release; 272 | }; 273 | F98F4DA32989B50100D6ABE8 /* Debug */ = { 274 | isa = XCBuildConfiguration; 275 | buildSettings = { 276 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 277 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 278 | CODE_SIGN_ENTITLEMENTS = TestVOD/TestVOD.entitlements; 279 | CODE_SIGN_STYLE = Automatic; 280 | CURRENT_PROJECT_VERSION = 1; 281 | DEVELOPMENT_ASSET_PATHS = "\"TestVOD/Preview Content\""; 282 | DEVELOPMENT_TEAM = WQY45CL427; 283 | ENABLE_HARDENED_RUNTIME = YES; 284 | ENABLE_PREVIEWS = YES; 285 | GENERATE_INFOPLIST_FILE = YES; 286 | INFOPLIST_FILE = TestVOD/Info.plist; 287 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 288 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 289 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 290 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 291 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 292 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 293 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 294 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 295 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 296 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 297 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 298 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 299 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 300 | MACOSX_DEPLOYMENT_TARGET = 13.1; 301 | MARKETING_VERSION = 1.0; 302 | PRODUCT_BUNDLE_IDENTIFIER = com.qoli.TestVOD; 303 | PRODUCT_NAME = "$(TARGET_NAME)"; 304 | SDKROOT = auto; 305 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 306 | SUPPORTS_MACCATALYST = YES; 307 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 308 | SWIFT_EMIT_LOC_STRINGS = YES; 309 | SWIFT_VERSION = 5.0; 310 | TARGETED_DEVICE_FAMILY = "1,2"; 311 | }; 312 | name = Debug; 313 | }; 314 | F98F4DA42989B50100D6ABE8 /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 318 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 319 | CODE_SIGN_ENTITLEMENTS = TestVOD/TestVOD.entitlements; 320 | CODE_SIGN_STYLE = Automatic; 321 | CURRENT_PROJECT_VERSION = 1; 322 | DEVELOPMENT_ASSET_PATHS = "\"TestVOD/Preview Content\""; 323 | DEVELOPMENT_TEAM = WQY45CL427; 324 | ENABLE_HARDENED_RUNTIME = YES; 325 | ENABLE_PREVIEWS = YES; 326 | GENERATE_INFOPLIST_FILE = YES; 327 | INFOPLIST_FILE = TestVOD/Info.plist; 328 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 329 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 330 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 331 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 332 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 333 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 334 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 335 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 336 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 337 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 338 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 339 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 340 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 341 | MACOSX_DEPLOYMENT_TARGET = 13.1; 342 | MARKETING_VERSION = 1.0; 343 | PRODUCT_BUNDLE_IDENTIFIER = com.qoli.TestVOD; 344 | PRODUCT_NAME = "$(TARGET_NAME)"; 345 | SDKROOT = auto; 346 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 347 | SUPPORTS_MACCATALYST = YES; 348 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 349 | SWIFT_EMIT_LOC_STRINGS = YES; 350 | SWIFT_VERSION = 5.0; 351 | TARGETED_DEVICE_FAMILY = "1,2"; 352 | }; 353 | name = Release; 354 | }; 355 | /* End XCBuildConfiguration section */ 356 | 357 | /* Begin XCConfigurationList section */ 358 | F98F4D8E2989B4FE00D6ABE8 /* Build configuration list for PBXProject "TestVOD" */ = { 359 | isa = XCConfigurationList; 360 | buildConfigurations = ( 361 | F98F4DA02989B50100D6ABE8 /* Debug */, 362 | F98F4DA12989B50100D6ABE8 /* Release */, 363 | ); 364 | defaultConfigurationIsVisible = 0; 365 | defaultConfigurationName = Release; 366 | }; 367 | F98F4DA22989B50100D6ABE8 /* Build configuration list for PBXNativeTarget "TestVOD" */ = { 368 | isa = XCConfigurationList; 369 | buildConfigurations = ( 370 | F98F4DA32989B50100D6ABE8 /* Debug */, 371 | F98F4DA42989B50100D6ABE8 /* Release */, 372 | ); 373 | defaultConfigurationIsVisible = 0; 374 | defaultConfigurationName = Release; 375 | }; 376 | /* End XCConfigurationList section */ 377 | }; 378 | rootObject = F98F4D8B2989B4FE00D6ABE8 /* Project object */; 379 | } 380 | -------------------------------------------------------------------------------- /TestVOD.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TestVOD.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TestVOD.xcodeproj/project.xcworkspace/xcuserdata/ronnie.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qoli/TestVOD/40cd67c7df57ac8582b9cc5621e0d6a210266096/TestVOD.xcodeproj/project.xcworkspace/xcuserdata/ronnie.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /TestVOD.xcodeproj/xcshareddata/xcschemes/TestVOD.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 67 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /TestVOD.xcodeproj/xcuserdata/qoli.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TestVOD.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | F98F4D922989B4FE00D6ABE8 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /TestVOD/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TestVOD/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /TestVOD/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TestVOD/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // TestVOD 4 | // 5 | // Created by 黃佁媛 on 2023/2/1. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @AppStorage("vodText") private var vodText: String = "" 12 | @AppStorage("reg") var reg: String = #"http(.*)\/api\.php\/provide\/vod\/at\/xml"# 13 | @AppStorage("keyword") var keyword: String = "娱乐百分百" 14 | 15 | @State var result: [String] = [] 16 | 17 | @State var show: Bool = false 18 | @State var text: String = "等待開始" 19 | @State var textL: String = "等待開始" 20 | @State var textR: String = "等待開始" 21 | 22 | 23 | 24 | var body: some View { 25 | List { 26 | Section(header: Text("匹配正則")) { 27 | TextField("正則", text: $reg) 28 | } 29 | 30 | Section(header: Text("內容")) { 31 | TextField("Keyword", text: $keyword) 32 | TextEditor(text: $vodText) 33 | .frame(height: 300) 34 | } 35 | 36 | Button { 37 | Task { 38 | await appear() 39 | } 40 | } label: { 41 | Text("開始測試") 42 | } 43 | 44 | Button { 45 | show.toggle() 46 | } label: { 47 | Text("顯示結果") 48 | } 49 | } 50 | 51 | .navigationTitle("VOD 地址格式測試") 52 | .sheet(isPresented: $show, content: { 53 | List { 54 | #if targetEnvironment(macCatalyst) 55 | Button { 56 | show.toggle() 57 | } label: { 58 | Text("關閉窗口") 59 | } 60 | #endif 61 | 62 | Section(header: Text("上次結果")) { 63 | Text(textL) 64 | .lineLimit(1) 65 | .font(.caption2) 66 | 67 | Text(textR) 68 | .font(.caption2) 69 | .lineLimit(5) 70 | } 71 | 72 | Section(header: Text("正在測試")) { 73 | HStack { 74 | Text(text) 75 | .font(.caption2) 76 | .lineLimit(1) 77 | 78 | Spacer() 79 | 80 | Text("\(i)/\(c)") 81 | .font(.caption2) 82 | } 83 | } 84 | 85 | Section(header: Text("通過測試的 VOD")) { 86 | if result.count == 0 { 87 | Text("尚未發現") 88 | } 89 | 90 | ForEach(result, id: \.self) { api in 91 | HStack { 92 | Text(api) 93 | .font(.caption2) 94 | .textSelection(.enabled) 95 | .lineLimit(1) 96 | 97 | Spacer() 98 | 99 | Button { 100 | #if os(iOS) 101 | UIPasteboard.general.string = api 102 | #endif 103 | 104 | #if os(macOS) 105 | let pasteboard = NSPasteboard.general 106 | pasteboard.declareTypes([.string], owner: nil) 107 | pasteboard.setString(api, forType: .string) 108 | #endif 109 | } label: { 110 | Text("複製") 111 | } 112 | } 113 | } 114 | } 115 | } 116 | .animation(.easeInOut, value: text) 117 | .animation(.easeInOut, value: result.count) 118 | }) 119 | } 120 | 121 | @State var i = 0 122 | @State var c = 0 123 | 124 | func appear() async { 125 | show = true 126 | 127 | let list = vodText.regex(for: reg) 128 | 129 | c = list.count 130 | 131 | for str in list { 132 | i = i + 1 133 | if let apiURL = URL(string: str.first ?? "") { 134 | text = apiURL.description 135 | print(apiURL.description) 136 | if let time = await apiURL.responseTimeAsync() { 137 | print("...", time.description) 138 | let r = await search(keyword: keyword, vodURL: apiURL.description) 139 | 140 | textL = apiURL.description 141 | textR = "\(r ?? "nil")" 142 | 143 | if r?.contains("m3u8") == true { 144 | print("### ->", apiURL.description, "<- ###") 145 | result.append(apiURL.description) 146 | print(r ?? "nil") 147 | print("... ### ...") 148 | } else { 149 | let rr = r?.regex(for: #"(.*)"#) 150 | print("No M3U8", rr?.first?.first ?? "no title") 151 | } 152 | } else { 153 | print("... timeout") 154 | textL = apiURL.description 155 | textR = "timeout" 156 | } 157 | } 158 | } 159 | } 160 | 161 | func search(keyword: String, vodURL: String) async -> String? { 162 | let para = [ 163 | "wd": keyword, 164 | "ac": "videolist", 165 | ] 166 | 167 | let headers: [String: String] = [ 168 | "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1", 169 | "Content-Type": "text/xml; charset=utf-8", 170 | ] 171 | 172 | let api = await api(withURLString: vodURL, parameters: para) 173 | await api.setFixUTF8() 174 | await api.setHeaders(headers: headers) 175 | let xmlString = try? await api.exec() 176 | 177 | return xmlString 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /TestVOD/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /TestVOD/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TestVOD/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String.swift 3 | // SyncNext 4 | // 5 | // Created by 黃佁媛 on 2021/10/14. 6 | // 7 | 8 | import CryptoKit 9 | import Foundation 10 | 11 | extension String { 12 | var isValidURL: Bool { 13 | let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) 14 | if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: utf16.count)) { 15 | // it is a link, if the match covers the whole string 16 | return match.range.length == utf16.count 17 | } else { 18 | return false 19 | } 20 | } 21 | 22 | func md5() -> String { 23 | return Insecure.MD5.hash(data: data(using: .utf8)!).map { String(format: "%02hhx", $0) }.joined() 24 | } 25 | 26 | func isValidEmailAddress() -> Bool { 27 | var returnValue = true 28 | let emailRegEx = "[A-Z0-9a-z.-_]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,3}" 29 | 30 | do { 31 | let regex = try NSRegularExpression(pattern: emailRegEx) 32 | let nsString = self as NSString 33 | let results = regex.matches(in: self, range: NSRange(location: 0, length: nsString.length)) 34 | 35 | if results.count == 0 { 36 | returnValue = false 37 | } 38 | 39 | } catch let error as NSError { 40 | print("invalid regex: \(error.localizedDescription)") 41 | returnValue = false 42 | } 43 | 44 | return returnValue 45 | } 46 | 47 | func regex(for regexPattern: String) -> [[String]] { 48 | do { 49 | let text = self 50 | let regex = try NSRegularExpression(pattern: regexPattern) 51 | let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text)) 52 | return matches.map { match in 53 | (0 ..< match.numberOfRanges).map { 54 | let rangeBounds = match.range(at: $0) 55 | guard let range = Range(rangeBounds, in: text) else { 56 | return "" 57 | } 58 | return String(text[range]) 59 | } 60 | } 61 | } catch let error { 62 | print("invalid regex: \(error.localizedDescription)") 63 | return [] 64 | } 65 | } 66 | 67 | func keywordTrim() -> String { 68 | let str = self 69 | 70 | var range = str.range(of: "(") 71 | 72 | if str.contains("(") { 73 | range = str.range(of: "(") 74 | } 75 | 76 | if let sr = range?.lowerBound { 77 | if String(str[str.startIndex ..< sr]) == "" { 78 | return str 79 | } else { 80 | return String(str[str.startIndex ..< sr]) 81 | } 82 | } else { 83 | return str 84 | } 85 | } 86 | 87 | func nameRemoveSeason() -> String { 88 | if let o = regex(for: "第([^\\s*]+)").first?.first { 89 | return replacingbyRegex(of: o, with: "") 90 | } 91 | 92 | return self 93 | } 94 | 95 | func nameGetSeason() -> String? { 96 | if !contains("第") { 97 | return nil 98 | } 99 | 100 | if let o = regex(for: "第([^\\s*]+)季").first?.first { 101 | return o.regex(for: "[\\d]").joined().joined() 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func nameGetEp() -> String? { 108 | if !contains("第") { 109 | return nil 110 | } 111 | 112 | if let o = regex(for: "第([^\\s*]+)集").first?.first { 113 | return o.regex(for: "[\\d]").joined().joined() 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func nameRemoveVideoFormat() -> String { 120 | return uppercased().replacingOccurrences(of: "720P", with: "").replacingOccurrences(of: "1080P", with: "").replacingOccurrences(of: "4k", with: "").replacingOccurrences(of: "MP4", with: "") 121 | } 122 | 123 | var lines: [String] { 124 | return components(separatedBy: "\n") 125 | } 126 | 127 | public func replacingbyRegex(of pattern: String, with replacement: String, options: NSRegularExpression.Options = []) -> String { 128 | do { 129 | let regex = try NSRegularExpression(pattern: pattern, options: []) 130 | let range = NSRange(0 ..< utf16.count) 131 | return regex.stringByReplacingMatches(in: self, options: [], 132 | range: range, withTemplate: replacement) 133 | } catch { 134 | NSLog("replaceAll error: \(error)") 135 | return self 136 | } 137 | } 138 | } 139 | 140 | extension String { 141 | var htmlStripped: String { 142 | return replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil) 143 | } 144 | 145 | func subtitleTrim() -> String { 146 | var text = htmlStripped.replacingbyRegex(of: "‎", with: "") 147 | let reg = text.regex(for: #"position:.* align:.* size:.* line:.*"#).joined().joined() 148 | text = text.replacingbyRegex(of: reg, with: "") 149 | let reg2 = text.regex(for: #"line:.*"#).joined().joined() 150 | text = text.replacingbyRegex(of: reg2, with: "") 151 | return text.trimmingCharacters(in: .whitespacesAndNewlines) 152 | } 153 | 154 | var containsChineseCharacters: Bool { 155 | return range(of: "\\p{Han}", options: .regularExpression) != nil 156 | } 157 | } 158 | 159 | extension String? { 160 | func UIText(_ text: String) -> String { 161 | if self == nil { 162 | return text 163 | } else { 164 | return self ?? text 165 | } 166 | } 167 | } 168 | 169 | extension String { 170 | func UIText(_ text: String) -> String { 171 | if self == "" { 172 | return text 173 | } else { 174 | return self 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /TestVOD/TestVOD.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TestVOD/TestVODApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestVODApp.swift 3 | // TestVOD 4 | // 5 | // Created by 黃佁媛 on 2023/2/1. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct TestVODApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | .withHostingWindow { window in 16 | #if targetEnvironment(macCatalyst) 17 | if let titlebar = window?.windowScene?.titlebar { 18 | titlebar.titleVisibility = .hidden 19 | titlebar.toolbar = nil 20 | } 21 | #endif 22 | } 23 | } 24 | } 25 | } 26 | 27 | extension View { 28 | fileprivate func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View { 29 | background(HostingWindowFinder(callback: callback)) 30 | } 31 | } 32 | 33 | fileprivate struct HostingWindowFinder: UIViewRepresentable { 34 | var callback: (UIWindow?) -> Void 35 | 36 | func makeUIView(context: Context) -> UIView { 37 | let view = UIView() 38 | DispatchQueue.main.async { [weak view] in 39 | self.callback(view?.window) 40 | } 41 | return view 42 | } 43 | 44 | func updateUIView(_ uiView: UIView, context: Context) { 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TestVOD/URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL.swift 3 | // SyncNext 4 | // 5 | // Created by 黃佁媛 on 2021/11/2. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol URLQueryParameterStringConvertible { 11 | var queryParameters: String { get } 12 | } 13 | 14 | extension Dictionary: URLQueryParameterStringConvertible { 15 | /** 16 | This computed property returns a query parameters string from the given NSDictionary. For 17 | example, if the input is @{@"day":@"Tuesday", @"month":@"January"}, the output 18 | string will be @"day=Tuesday&month=January". 19 | @return The computed parameters string. 20 | */ 21 | var queryParameters: String { 22 | var parts: [String] = [] 23 | for (key, value) in self { 24 | let part = String(format: "%@=%@", 25 | String(describing: key).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!, 26 | String(describing: value).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) 27 | parts.append(part as String) 28 | } 29 | return parts.joined(separator: "&") 30 | } 31 | } 32 | 33 | extension URL { 34 | /** 35 | Creates a new URL by adding the given query parameters. 36 | @param parametersDictionary The query parameter dictionary to add. 37 | @return A new URL. 38 | */ 39 | func appendingQueryParameters(_ parametersDictionary: Dictionary) -> URL { 40 | let URLString: String = String(format: "%@?%@", absoluteString, parametersDictionary.queryParameters) 41 | return URL(string: URLString)! 42 | } 43 | } 44 | 45 | extension URL { 46 | func queryOf(_ queryParameterName: String) -> String? { 47 | guard let url = URLComponents(string: absoluteString) else { return nil } 48 | return url.queryItems?.first(where: { $0.name == queryParameterName })?.value 49 | } 50 | } 51 | 52 | extension URL { 53 | /** Request the http status of the URL resource by sending a "HEAD" request over the network. A nil response means an error occurred. */ 54 | public func requestHTTPStatus(completion: @escaping (_ status: Int?) -> Void) { 55 | // Adapted from https://stackoverflow.com/a/35720670/7488171 56 | var request = URLRequest(url: self) 57 | request.httpMethod = "HEAD" 58 | let task = URLSession.shared.dataTask(with: request) { _, response, error in 59 | if let httpResponse = response as? HTTPURLResponse, error == nil { 60 | completion(httpResponse.statusCode) 61 | } else { 62 | completion(nil) 63 | } 64 | } 65 | task.resume() 66 | } 67 | 68 | /** Measure the response time in seconds of an http "HEAD" request to the URL resource. A nil response means an error occurred. */ 69 | public func responseTime(completion: @escaping (TimeInterval?) -> Void) { 70 | let startTime = DispatchTime.now().uptimeNanoseconds 71 | requestHTTPStatus { status in 72 | if status != nil { 73 | let elapsedNanoseconds = DispatchTime.now().uptimeNanoseconds - startTime 74 | completion(TimeInterval(elapsedNanoseconds) / 1e9) 75 | } else { 76 | completion(nil) 77 | } 78 | } 79 | } 80 | 81 | public func responseTimeAsync() async -> TimeInterval? { 82 | let time = await withCheckedContinuation { continuation in 83 | DispatchQueue.global().async { 84 | self.responseTime { time in 85 | continuation.resume(returning: time) 86 | } 87 | } 88 | } 89 | 90 | return time 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /TestVOD/api.swift: -------------------------------------------------------------------------------- 1 | // 2 | // api.swift 3 | // SyncNext 4 | // 5 | // Created by 黃佁媛 on 2021/10/13. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array where Element == URLQueryItem { 11 | init(_ dictionary: [String: T]) { 12 | self = dictionary.map({ key, value -> Element in 13 | URLQueryItem(name: key, value: String(value)) 14 | }) 15 | } 16 | } 17 | 18 | @MainActor 19 | class api: NSObject, URLSessionDelegate { 20 | private var dataString: String? 21 | private let urlString: String 22 | private var parameters: [String: String] = [:] 23 | 24 | private var additionalHeadersDict = [ 25 | "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.14(0x18000e2d) NetType/WIFI Language/zh_HK", 26 | ] 27 | 28 | private var postData: [String: Any] = [:] 29 | 30 | init(withURLString urlString: String, parameters: [String: String]? = nil, encode: Bool = true) { 31 | if encode { 32 | self.urlString = urlString.trimmingCharacters(in: .whitespaces).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString 33 | } else { 34 | self.urlString = urlString 35 | } 36 | 37 | self.parameters = parameters ?? [:] 38 | } 39 | 40 | private var ignoreErrorUI = false 41 | func setErrorUI(ignore: Bool) { 42 | ignoreErrorUI = ignore 43 | } 44 | 45 | func setHeaders(headers: [String: String]) { 46 | additionalHeadersDict = headers 47 | } 48 | 49 | func setPostData(with data: [String: Any]) { 50 | postData = data 51 | } 52 | 53 | private var isPost: Bool = false 54 | func setPost() { 55 | isPost = true 56 | } 57 | 58 | private var fixUTF8: Bool = false 59 | func setFixUTF8() { 60 | fixUTF8 = true 61 | } 62 | 63 | private var isSSLPinning: Bool = false 64 | 65 | func setSSLPinning(_ bool: Bool) { 66 | isSSLPinning = bool 67 | } 68 | 69 | private func buildURL() -> URL? { 70 | if parameters.isEmpty { 71 | return URL(string: urlString) 72 | } 73 | 74 | var com = URLComponents(string: urlString) 75 | com?.queryItems = .init(parameters) 76 | 77 | return com?.url 78 | } 79 | 80 | @MainActor private func oldCompletionMethod(completion: @escaping () -> Void) { 81 | guard var url = buildURL() else { 82 | completion() 83 | return 84 | } 85 | 86 | if fixUTF8 { 87 | let urlString = url.description.replacingbyRegex(of: "%EF%BF%BC", with: "") 88 | if let newURL = URL(string: urlString) { 89 | url = newURL 90 | } 91 | } 92 | 93 | let sessionConfiguration = URLSessionConfiguration.default 94 | sessionConfiguration.timeoutIntervalForRequest = TimeInterval(5) 95 | sessionConfiguration.timeoutIntervalForResource = TimeInterval(5) 96 | sessionConfiguration.httpAdditionalHeaders = additionalHeadersDict 97 | 98 | var session = URLSession(configuration: sessionConfiguration) 99 | 100 | if isSSLPinning { 101 | print("api isSSLPinning", "...", isSSLPinning) 102 | session = URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: OperationQueue.main) 103 | } 104 | 105 | var request = URLRequest(url: url) 106 | 107 | var thisPost = false 108 | 109 | if !postData.isEmpty || isPost { 110 | thisPost = true 111 | do { 112 | request.httpMethod = "POST" 113 | request.httpBody = try JSONSerialization.data(withJSONObject: postData, options: []) 114 | 115 | } catch { 116 | debugPrint(error) 117 | } 118 | } 119 | 120 | print("api", !thisPost ? "GET" : "POST", "...", request) 121 | 122 | if request.description.contains("%EF%BF%BC") { 123 | print("api", "...", "!!! %EF%BF%BC 存在!!!") 124 | } 125 | 126 | session.dataTask(with: request) { data, _, error in 127 | if let error = error { 128 | debugPrint(error) 129 | DispatchQueue.main.async { [self] in 130 | showError(url: url, error: error) 131 | } 132 | completion() 133 | } 134 | 135 | if let data = data { 136 | DispatchQueue.main.async { [self] in 137 | dataString = String(data: data, encoding: .utf8) 138 | } 139 | completion() 140 | } 141 | }.resume() 142 | } 143 | 144 | private func asyncTask() async { 145 | await withUnsafeContinuation { task in 146 | oldCompletionMethod { 147 | task.resume() 148 | } 149 | } 150 | } 151 | 152 | func exec() async throws -> String? { 153 | await asyncTask() 154 | return dataString 155 | } 156 | 157 | func showError(url: URL, error: Error) { 158 | if ignoreErrorUI { 159 | return 160 | } 161 | } 162 | 163 | nonisolated func urlSession( 164 | _ session: URLSession, 165 | didReceive challenge: URLAuthenticationChallenge, 166 | completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void 167 | ) { 168 | if challenge.protectionSpace.host != "prodssl.mddcloud.com.cn" { 169 | print(#function, "don't need a client certificate") 170 | completionHandler(.performDefaultHandling, nil) 171 | return 172 | } 173 | 174 | // `NSURLAuthenticationMethodClientCertificate` 175 | // indicates the server requested a client certificate. 176 | 177 | guard 178 | let file = Bundle.main.url(forResource: "purchase_client", withExtension: "p12"), 179 | let p12Data = try? Data(contentsOf: file) 180 | else { 181 | // Loading of the p12 file's data failed. 182 | print(#function, "...", "Loading of the p12 file's data failed.") 183 | completionHandler(.performDefaultHandling, nil) 184 | return 185 | } 186 | 187 | // Interpret the data in the P12 data blob with 188 | // a little helper class called `PKCS12`. 189 | let password = "TvBc@1234" // Obviously this should be stored or entered more securely. 190 | let p12Contents = PKCS12(pkcs12Data: p12Data, password: password) 191 | guard let identity = p12Contents.identity else { 192 | // Creating a PKCS12 never fails, but interpretting th contained data can. So again, no identity? We fall back to default. 193 | print(#function, "...", "Creating a PKCS12 never fails") 194 | completionHandler(.performDefaultHandling, nil) 195 | return 196 | } 197 | 198 | // In my case, and as Apple recommends, 199 | // we do not pass the certificate chain into 200 | // the URLCredential used to respond to the challenge. 201 | print(#function, "...", "the URLCredential used to respond to the challenge.") 202 | let credential = URLCredential(identity: identity, certificates: nil, persistence: .none) 203 | challenge.sender?.use(credential, for: challenge) 204 | completionHandler(.useCredential, credential) 205 | } 206 | } 207 | 208 | private class PKCS12 { 209 | let label: String? 210 | let keyID: NSData? 211 | let trust: SecTrust? 212 | let certChain: [SecTrust]? 213 | let identity: SecIdentity? 214 | 215 | /// Creates a PKCS12 instance from a piece of data. 216 | /// - Parameters: 217 | /// - pkcs12Data: the actual data we want to parse. 218 | /// - password: The password required to unlock the PKCS12 data. 219 | public init(pkcs12Data: Data, password: String) { 220 | let importPasswordOption: NSDictionary 221 | = [kSecImportExportPassphrase as NSString: password] 222 | var items: CFArray? 223 | let secError: OSStatus 224 | = SecPKCS12Import(pkcs12Data as NSData, 225 | importPasswordOption, &items) 226 | guard secError == errSecSuccess else { 227 | if secError == errSecAuthFailed { 228 | NSLog("Incorrect password?") 229 | } 230 | fatalError("Error trying to import PKCS12 data") 231 | } 232 | guard let theItemsCFArray = items else { fatalError() } 233 | let theItemsNSArray: NSArray = theItemsCFArray as NSArray 234 | guard let dictArray 235 | = theItemsNSArray as? [[String: AnyObject]] else { 236 | fatalError() 237 | } 238 | func f(key: CFString) -> T? { 239 | for dict in dictArray { 240 | if let value = dict[key as String] as? T { 241 | return value 242 | } 243 | } 244 | return nil 245 | } 246 | label = f(key: kSecImportItemLabel) 247 | keyID = f(key: kSecImportItemKeyID) 248 | trust = f(key: kSecImportItemTrust) 249 | certChain = f(key: kSecImportItemCertChain) 250 | identity = f(key: kSecImportItemIdentity) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /assets/SCR-20230207-n48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qoli/TestVOD/40cd67c7df57ac8582b9cc5621e0d6a210266096/assets/SCR-20230207-n48.png -------------------------------------------------------------------------------- /assets/SCR-20230207-ncx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qoli/TestVOD/40cd67c7df57ac8582b9cc5621e0d6a210266096/assets/SCR-20230207-ncx.png -------------------------------------------------------------------------------- /assets/SCR-20230207-nfi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qoli/TestVOD/40cd67c7df57ac8582b9cc5621e0d6a210266096/assets/SCR-20230207-nfi.png --------------------------------------------------------------------------------