Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexStich committed Feb 15, 2023
0 parents commit 7804ef5
Show file tree
Hide file tree
Showing 11 changed files with 737 additions and 0 deletions.
36 changes: 36 additions & 0 deletions .gitignore
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/
14 changes: 14 additions & 0 deletions .travis.yml
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
42 changes: 42 additions & 0 deletions AGVideoLoader.podspec
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 added AGVideoLoader/Assets/.gitkeep
Empty file.
Empty file added AGVideoLoader/Classes/.gitkeep
Empty file.
252 changes: 252 additions & 0 deletions AGVideoLoader/Classes/AGCacheProvider.swift
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
}
}
}
32 changes: 32 additions & 0 deletions AGVideoLoader/Classes/AGLogHelper.swift
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)
}
}
}
Loading

0 comments on commit 7804ef5

Please sign in to comment.