-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
AlexStich
committed
Feb 15, 2023
0 parents
commit 7804ef5
Showing
11 changed files
with
737 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# macOS | ||
.DS_Store | ||
|
||
# Xcode | ||
build/ | ||
*.pbxuser | ||
!default.pbxuser | ||
*.mode1v3 | ||
!default.mode1v3 | ||
*.mode2v3 | ||
!default.mode2v3 | ||
*.perspectivev3 | ||
!default.perspectivev3 | ||
xcuserdata/ | ||
*.xccheckout | ||
*.moved-aside | ||
DerivedData | ||
*.hmap | ||
*.ipa | ||
|
||
# Bundler | ||
.bundle | ||
|
||
# Add this line if you want to avoid checking in source code from Carthage dependencies. | ||
# Carthage/Checkouts | ||
|
||
Carthage/Build | ||
|
||
# We recommend against adding the Pods directory to your .gitignore. However | ||
# you should judge for yourself, the pros and cons are mentioned at: | ||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control | ||
# | ||
# Note: if you ignore the Pods directory, make sure to uncomment | ||
# `pod install` in .travis.yml | ||
# | ||
# Pods/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# references: | ||
# * https://www.objc.io/issues/6-build-tools/travis-ci/ | ||
# * https://github.com/supermarin/xcpretty#usage | ||
|
||
osx_image: xcode7.3 | ||
language: objective-c | ||
# cache: cocoapods | ||
# podfile: Example/Podfile | ||
# before_install: | ||
# - gem install cocoapods # Since Travis is not always on latest version | ||
# - pod install --project-directory=Example | ||
script: | ||
- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/AGVideoLoader.xcworkspace -scheme AGVideoLoader-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty | ||
- pod lib lint |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# | ||
# Be sure to run `pod lib lint AGVideoLoader.podspec' to ensure this is a | ||
# valid spec before submitting. | ||
# | ||
# Any lines starting with a # are optional, but their use is encouraged | ||
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html | ||
# | ||
|
||
Pod::Spec.new do |s| | ||
s.name = 'AGVideoLoader' | ||
s.version = '1.0.0' | ||
s.summary = 'It prefetch video in UITableView and cache it' | ||
|
||
# This description is used to generate tags and improve search results. | ||
# * Think: What does it do? Why did you write it? What is the focus? | ||
# * Try to keep it short, snappy and to the point. | ||
# * Write the description between the DESC delimiters below. | ||
# * Finally, don't worry about the indent, CocoaPods strips it! | ||
|
||
s.description = <<-DESC | ||
It prefetch video in UITableView and cache it. | ||
DESC | ||
|
||
s.homepage = 'https://github.com/AlexStich/AGVideoLoader' | ||
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' | ||
s.license = { :type => 'MIT', :file => 'LICENSE' } | ||
s.author = { 'AlexStich' => '[email protected]' } | ||
s.source = { :git => 'https://github.com/AlexStich/AGVideoLoader.git', :tag => s.version.to_s } | ||
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>' | ||
|
||
s.ios.deployment_target = '12.0' | ||
|
||
s.source_files = 'AGVideoLoader/**/*' | ||
|
||
# s.resource_bundles = { | ||
# 'AGVideoLoader' => ['AGVideoLoader/Assets/*.png'] | ||
# } | ||
|
||
# s.public_header_files = 'Pod/Classes/**/*.h' | ||
# s.frameworks = 'UIKit', 'MapKit' | ||
# s.dependency 'AFNetworking', '~> 2.3' | ||
end |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
// | ||
// CacheManager.swift | ||
// Babydaika | ||
// | ||
// Created by Алексей Гребенкин on 01.02.2023. | ||
// Copyright © 2023 dimfcompany. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import AVFoundation | ||
|
||
class AGCacheProvider: NSObject, AVAssetResourceLoaderDelegate | ||
{ | ||
struct Config | ||
{ | ||
var cacheDirectoryName: String = "AGCache" | ||
/// MB | ||
var maxCacheDirectorySpace: Int = 200 * 1024 * 1024 | ||
} | ||
|
||
var config: Config! | ||
var cacheDirectory: URL? | ||
|
||
init(_ config: Config? = nil) | ||
{ | ||
if config != nil { | ||
self.config = config | ||
} else { | ||
self.config = AGCacheProvider.Config() | ||
} | ||
|
||
super.init() | ||
|
||
try? self.prepareStorageDirectory() | ||
} | ||
|
||
func applyConfig(_ config: Config) | ||
{ | ||
self.config = config | ||
} | ||
|
||
/// Creates if needed the cache directory | ||
func prepareStorageDirectory() throws { | ||
|
||
var cacheURL = FileManager.default.urls(for: .cachesDirectory,in: .userDomainMask).first | ||
|
||
cacheURL = cacheURL?.appendingPathComponent("tech.avgrebenkin.\(config.cacheDirectoryName).cache", isDirectory: true) | ||
cacheURL = cacheURL?.appendingPathComponent("videos", isDirectory: true) | ||
|
||
guard let path = cacheURL?.path, !FileManager.default.fileExists(atPath: path) else { return cacheDirectory = cacheURL } | ||
guard (try? FileManager.default.createDirectory(at: cacheURL!, withIntermediateDirectories: true)) != nil else { return } | ||
|
||
cacheDirectory = cacheURL | ||
} | ||
|
||
/// Creates an output path | ||
/// | ||
/// - Parameters: | ||
/// - url: file url for export | ||
private func getCacheURLPath(url: URL) -> URL? { | ||
|
||
var outputURL: URL? | ||
outputURL = cacheDirectory?.appendingPathComponent(url.lastPathComponent, isDirectory: false) | ||
return outputURL | ||
} | ||
|
||
/// Creates an output path | ||
/// | ||
/// - Parameters: | ||
/// - name: file name for export | ||
private func getCacheURLPath(name: String) -> URL? { | ||
|
||
var outputURL: URL? | ||
outputURL = cacheDirectory?.appendingPathComponent(name, isDirectory: false) | ||
return outputURL | ||
} | ||
|
||
func checkCacheUrl(url: URL) -> URL? | ||
{ | ||
guard let cacheURLPath = self.getCacheURLPath(url: url) else { return nil } | ||
guard FileManager.default.fileExists(atPath: cacheURLPath.path) else { return nil } | ||
|
||
return cacheURLPath | ||
} | ||
|
||
func store(asset: AVURLAsset) | ||
{ | ||
guard self.cacheDirectory != nil else { return } | ||
guard let cacheURLPath = self.getCacheURLPath(url: asset.url) else { return } | ||
|
||
AGLogHelper.instance.printToConsole("Try to cache asset \(asset.url.path.suffix(10))") | ||
|
||
DispatchQueue.global(qos: .userInitiated).async { | ||
let data = try? Data(contentsOf: asset.url) | ||
guard data != nil, self.freeCacheDirectorySpace(for: data!) else { return } | ||
let result = FileManager.default.createFile(atPath: cacheURLPath.path , contents: data, attributes: nil) | ||
|
||
AGLogHelper.instance.printToConsole("Assets cached \(asset.url.path.suffix(10)) - \(result)") | ||
} | ||
} | ||
|
||
func store(data: Data, name: String) | ||
{ | ||
guard self.cacheDirectory != nil else { return } | ||
guard let cacheURLPath = self.getCacheURLPath(name: name) else { return } | ||
|
||
DispatchQueue.global(qos: .userInitiated).async { | ||
guard self.freeCacheDirectorySpace(for: data) else { return } | ||
let result = FileManager.default.createFile(atPath: cacheURLPath.path , contents: data, attributes: nil) | ||
|
||
AGLogHelper.instance.printToConsole("Assets cached \(name) - \(result)") | ||
} | ||
|
||
|
||
// asset.resourceLoader.setDelegate(self, queue: .main) | ||
// let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) | ||
// exporter?.outputURL = cacheNamePathURL | ||
// exporter?.outputFileType = AVFileType.mp4 | ||
// | ||
// exporter?.exportAsynchronously(completionHandler: { | ||
// print("**** export work") | ||
// print(exporter?.status.rawValue) | ||
// print(exporter?.error) | ||
// }) | ||
} | ||
|
||
func freeCacheDirectorySpace(for data: Data) -> Bool | ||
{ | ||
let ceil__ = ceil(Float(data.count)/(1024*1024)) | ||
AGLogHelper.instance.printToConsole("New file space - \(ceil__)") | ||
|
||
guard self.cacheDirectory != nil else { return false } | ||
guard data.count < config.maxCacheDirectorySpace else { return false } | ||
|
||
var totalSpace = self.cacheDirectory!.folderSize() | ||
|
||
let ceil_ = ceil(Float(config.maxCacheDirectorySpace)/(1024*1024)) - ceil(Float(totalSpace)/(1024*1024)) | ||
AGLogHelper.instance.printToConsole("Total space before store to cache - \(ceil_)") | ||
|
||
if (totalSpace + data.count) > config.maxCacheDirectorySpace { | ||
if var directoryContents = try? FileManager.default.contentsOfDirectory( | ||
at: self.cacheDirectory!, | ||
includingPropertiesForKeys: [.totalFileSizeKey] | ||
) { | ||
|
||
directoryContents.sort(by: { (url_a, url_b) in | ||
return url_a.creationDate()! <= url_b.creationDate()! | ||
}) | ||
|
||
directoryContents.map({ url in | ||
AGLogHelper.instance.printToConsole("File in directory - \(url.path.suffix(10)) - \(String(describing: url.creationDate()))") | ||
}) | ||
|
||
for url in directoryContents { | ||
|
||
AGLogHelper.instance.printToConsole("File need to remove - \(url.path.suffix(10)) - \(String(describing: url.creationDate()))") | ||
|
||
let values = try? url.resourceValues(forKeys: [.totalFileSizeKey]) | ||
let size = values?.totalFileSize | ||
|
||
if size != nil { | ||
totalSpace -= size! | ||
try? FileManager.default.removeItem(atPath: url.path) | ||
|
||
let ceil = ceil(Float(totalSpace)/(1024*1024)) | ||
AGLogHelper.instance.printToConsole("Total space after removing file - \(ceil)") | ||
|
||
if (totalSpace + data.count) < config.maxCacheDirectorySpace { | ||
break | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
return true | ||
} | ||
|
||
func clearCache() | ||
{ | ||
guard self.cacheDirectory != nil else { return } | ||
|
||
let totalSpace = self.cacheDirectory!.folderSize() | ||
let ceil_ = ceil(Float(totalSpace/(1024*1024))) | ||
AGLogHelper.instance.printToConsole("Space before clearing cache - \(ceil_)") | ||
|
||
try? FileManager.default.removeItem(atPath: self.cacheDirectory!.path) | ||
} | ||
} | ||
|
||
|
||
extension URL { | ||
public func directoryContents() -> [URL] { | ||
do { | ||
let directoryContents = try FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil) | ||
return directoryContents | ||
} catch let error { | ||
print("Error: \(error)") | ||
return [] | ||
} | ||
} | ||
|
||
public func folderSize() -> Int { | ||
let contents = self.directoryContents() | ||
var totalSize: Int = 0 | ||
contents.forEach { url in | ||
let size = url.fileSize() | ||
totalSize += size | ||
} | ||
return totalSize | ||
} | ||
|
||
public func fileSize() -> Int { | ||
let attributes = URLFileAttribute(url: self) | ||
return attributes.fileSize ?? 0 | ||
} | ||
|
||
public func creationDate() -> Date? { | ||
let attributes = URLFileAttribute(url: self) | ||
return attributes.creationDate | ||
} | ||
} | ||
|
||
// MARK: - URLFileAttribute | ||
struct URLFileAttribute { | ||
private(set) var fileSize: Int? = nil | ||
private(set) var creationDate: Date? = nil | ||
private(set) var modificationDate: Date? = nil | ||
|
||
init(url: URL) { | ||
let path = url.path | ||
guard let dictionary: [FileAttributeKey: Any] = try? FileManager.default | ||
.attributesOfItem(atPath: path) else { | ||
return | ||
} | ||
|
||
if dictionary.keys.contains(FileAttributeKey.size), | ||
let value = dictionary[FileAttributeKey.size] as? Int { | ||
self.fileSize = value | ||
} | ||
|
||
if dictionary.keys.contains(FileAttributeKey.creationDate), | ||
let value = dictionary[FileAttributeKey.creationDate] as? Date { | ||
self.creationDate = value | ||
} | ||
|
||
if dictionary.keys.contains(FileAttributeKey.modificationDate), | ||
let value = dictionary[FileAttributeKey.modificationDate] as? Date { | ||
self.modificationDate = value | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// | ||
// AGLogHelper.swift | ||
// Babydaika | ||
// | ||
// Created by Алексей Гребенкин on 14.02.2023. | ||
// Copyright © 2023 dimfcompany. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import SwiftyBeaver | ||
|
||
class AGLogHelper | ||
{ | ||
static let instance = AGLogHelper() | ||
static var debugModeOn: Bool = false | ||
|
||
private init(){} | ||
|
||
func printToConsole(_ str: String) | ||
{ | ||
if AGLogHelper.debugModeOn { | ||
|
||
let dateFormatter = DateFormatter() | ||
dateFormatter.locale = Locale.current// Locale(identifier: "en_US_POSIX") | ||
dateFormatter.dateFormat = "HH:mm:ss" | ||
dateFormatter.timeZone = .current | ||
let dateString = dateFormatter.string(from: Date()) | ||
|
||
print(dateString + "🐙 AG *** " + str) | ||
} | ||
} | ||
} |
Oops, something went wrong.