Skip to content

Commit

Permalink
Add multiple export profile support with new Airtable profile (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
orchetect committed Jan 1, 2023
1 parent c021dd7 commit dfebc8d
Show file tree
Hide file tree
Showing 33 changed files with 541 additions and 376 deletions.
42 changes: 21 additions & 21 deletions Sources/MarkersExtractor/EmbeddedResource.swift

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions Sources/MarkersExtractor/Export/Components/CSV Export Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// CSV Export Utils.swift
// MarkersExtractor • https://github.com/TheAcharya/MarkersExtractor
// Licensed under MIT License
//

import Foundation
import OrderedCollections
import CodableCSV

extension ExportProfile {
static func csvWiteManifest(
csvPath: URL,
_ preparedMarkers: [PreparedMarker],
payload: Payload
) throws {
let rows = csvDictsToRows(preparedMarkers)
let csvData = try CSVWriter.encode(rows: rows, into: Data.self)
try csvData.write(to: csvPath)
}

public static func csvDoneFileContent(csvPath: URL) throws -> Data {
let content = ["csvPath": csvPath.path]

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
return try encoder.encode(content)
}

// MARK: Helpers

private static func csvDictsToRows(
_ preparedMarkers: [PreparedMarker]
) -> [[String]] {
let dicts = preparedMarkers.map { manifestFields(for: $0) }
guard !dicts.isEmpty else { return [] }

// header
var result = [Array(dicts[0].keys.map { $0.name })]

// marker rows
result += dicts.map { row in
Array(row.values)
}

return result
}
}
16 changes: 16 additions & 0 deletions Sources/MarkersExtractor/Export/Components/CSVExportPayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// AirtableExportProfile Payload.swift
// MarkersExtractor • https://github.com/TheAcharya/MarkersExtractor
// Licensed under MIT License
//

import Foundation

public struct CSVExportPayload: ExportPayload {
let csvPath: URL

init(projectName: String, outputPath: URL) {
let csvName = "\(projectName).csv"
csvPath = outputPath.appendingPathComponent(csvName)
}
}
18 changes: 18 additions & 0 deletions Sources/MarkersExtractor/Export/Components/EmptyExportIcon.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// EmptyExportIcon.swift
// MarkersExtractor • https://github.com/TheAcharya/MarkersExtractor
// Licensed under MIT License
//

import Foundation

/// `ExportIcon` prototype that can be used when a profile does not use marker icons.
public struct EmptyExportIcon: ExportIcon {
public var resource: EmbeddedResource = .notion_marker_png // ignore

public var fileName: String = "empty.png"

public let data: Data = Data()

public init(_ type: MarkerType) { }
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
//
// CSVExportProfile Field.swift
// StandardExportField.swift
// MarkersExtractor • https://github.com/TheAcharya/MarkersExtractor
// Licensed under MIT License
//

extension CSVExportProfile {
/// Markers CSV fields (header column names).
public enum Field: String, CaseIterable {
case id
case name
case type
case checked
case status
case notes
case position
case clipName
case clipDuration
case videoRoles
case audioRoles
case eventName
case projectName
case libraryName
case iconImage
case imageFileName
}
/// Markers CSV fields (header column names).
public enum StandardExportField: String, CaseIterable {
case id
case name
case type
case checked
case status
case notes
case position
case clipName
case clipDuration
case videoRoles
case audioRoles
case eventName
case projectName
case libraryName
case iconImage
case imageFileName
}

extension CSVExportProfile.Field: ExportField {
extension StandardExportField: ExportField {
public var name: String {
switch self {
case .id: return "Marker ID"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// StandardExportMarker.swift
// MarkersExtractor • https://github.com/TheAcharya/MarkersExtractor
// Licensed under MIT License
//

import Foundation
import OrderedCollections

/// A marker with its contents prepared as flat String values in a standard format suitable for
/// various different export profiles.
public struct StandardExportMarker: ExportMarker {
public typealias Field = StandardExportField
public typealias Icon = NotionExportProfile.Icon

public let id: String
public let name: String
public let type: String
public let checked: String
public let status: String
public let notes: String
public let position: String
public let clipName: String
public let clipDuration: String
public let audioRoles: String
public let videoRoles: String
public let eventName: String
public let projectName: String
public let libraryName: String
public let icon: Icon
public let imageFileName: String

public init(
_ marker: Marker,
idMode: MarkerIDMode,
imageFormat: MarkerImageFormat,
isSingleFrame: Bool
) {
id = marker.id(idMode)
name = marker.name
type = marker.type.name
checked = String(marker.isChecked())
status = NotionExportProfile.Status(marker.type).rawValue
notes = marker.notes
position = marker.positionTimecodeString()
clipName = marker.parentInfo.clipName
clipDuration = marker.parentInfo.clipDurationTimecodeString
videoRoles = marker.roles.flattenedString()
audioRoles = marker.roles.flattenedString()
eventName = marker.parentInfo.eventName
projectName = marker.parentInfo.projectName
libraryName = marker.parentInfo.libraryName
icon = Icon(marker.type)
imageFileName = isSingleFrame
? "marker-placeholder.\(imageFormat)"
: "\(marker.id(pathSafe: idMode)).\(imageFormat)"
}
}
2 changes: 1 addition & 1 deletion Sources/MarkersExtractor/Export/ExportField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import Foundation

public protocol ExportField: RawRepresentable, Hashable
public protocol ExportField: RawRepresentable, Hashable, CaseIterable
where RawValue == String {
/// Human-readable name. Useful for column name in exported tabular data.
var name: String { get }
Expand Down
14 changes: 14 additions & 0 deletions Sources/MarkersExtractor/Export/ExportIcon.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// ExportIcon.swift
// MarkersExtractor • https://github.com/TheAcharya/MarkersExtractor
// Licensed under MIT License
//

import Foundation

public protocol ExportIcon: Equatable, Hashable {
var resource: EmbeddedResource { get }
var fileName: String { get }
var data: Data { get }
init(_ type: MarkerType)
}
4 changes: 2 additions & 2 deletions Sources/MarkersExtractor/Export/ExportMarker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import OrderedCollections

public protocol ExportMarker {
associatedtype Field: ExportField
associatedtype Icon: ExportIcon

var imageFileName: String { get }

func dictionaryRepresentation() -> OrderedDictionary<Field, String>
var icon: Icon { get }
}
35 changes: 31 additions & 4 deletions Sources/MarkersExtractor/Export/ExportProfile Export.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ extension ExportProfile {
videoPath: URL,
outputPath: URL,
payload: Payload,
imageSettings: ExportImageSettings<Field>
imageSettings: ExportImageSettings<Field>,
createDoneFile: Bool,
doneFilename: String
) throws {
let logger = Logger(label: "markersExport")

Expand Down Expand Up @@ -107,6 +109,14 @@ extension ExportProfile {
// metadata manifest file

try writeManifest(preparedMarkers, payload: payload)

// done file

if createDoneFile {
logger.info("Creating \(doneFilename.quoted) done file at \(outputPath.path.quoted).")
let doneFileData = try doneFileContent(payload: payload)
try saveDoneFile(at: outputPath, fileName: doneFilename, data: doneFileData)
}
}

// MARK: Helpers
Expand Down Expand Up @@ -146,7 +156,7 @@ extension ExportProfile {
preparedMarkers: [PreparedMarker]
) -> [String] {
preparedMarkers
.map { $0.dictionaryRepresentation() }
.map { manifestFields(for: $0) }
.map { markerDict in
headers
.map {
Expand All @@ -169,7 +179,7 @@ extension ExportProfile {

// if no video - grabbing first frame from video placeholder
let markerTimecodes = markers.map {
isVideoPresent ? $0.position : .init(at: $0.frameRate)
isVideoPresent ? $0.position : .init(at: $0.frameRate())
}

var markerPairs = zip(imageFileNames, markerTimecodes).map { ($0, $1) }
Expand All @@ -183,14 +193,31 @@ extension ExportProfile {
}

private static func exportIcons(from markers: [Marker], to outputDir: URL) throws {
let icons = Set(markers.map { $0.icon })
let icons = Set(markers.map { Icon($0.type) })

for icon in icons {
if icon is EmptyExportIcon { continue }
let targetURL = outputDir.appendingPathComponent(icon.fileName)
try icon.data.write(to: targetURL)
}
}

private static func saveDoneFile(
at outputPath: URL,
fileName: String,
data: Data
) throws {
let doneFile = outputPath.appendingPathComponent(fileName)

do {
try data.write(to: doneFile)
} catch {
throw MarkersExtractorError.runtimeError(
"Failed to create done file \(doneFile.path.quoted): \(error.localizedDescription)"
)
}
}

private static func isVideoPresent(in videoPath: URL) -> Bool {
let asset = AVAsset(url: videoPath)

Expand Down
10 changes: 9 additions & 1 deletion Sources/MarkersExtractor/Export/ExportProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
//

import Foundation
import OrderedCollections

public protocol ExportProfile
where PreparedMarker.Field == Field
{
associatedtype Field: ExportField
associatedtype Payload: ExportPayload
associatedtype PreparedMarker: ExportMarker
associatedtype Icon: ExportIcon

/// Exports markers to disk.
/// Writes metadata files, images, and any other resources necessary.
Expand All @@ -21,7 +23,9 @@ public protocol ExportProfile
videoPath: URL,
outputPath: URL,
payload: Payload,
imageSettings: ExportImageSettings<Field>
imageSettings: ExportImageSettings<Field>,
createDoneFile: Bool,
doneFilename: String
) throws

/// Converts raw FCP markers to the native format needed for export.
Expand All @@ -38,4 +42,8 @@ public protocol ExportProfile
_ preparedMarkers: [PreparedMarker],
payload: Payload
) throws

static func doneFileContent(payload: Payload) throws -> Data

static func manifestFields(for marker: PreparedMarker) -> OrderedDictionary<Field, String>
}
12 changes: 12 additions & 0 deletions Sources/MarkersExtractor/Export/ExportProfileFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,17 @@
import Foundation

public enum ExportProfileFormat: String, CaseIterable {
case airtable = "airtable"
case notion = "notion"
}

extension ExportProfileFormat {
var name: String {
switch self {
case .airtable:
return "Airtable"
case .notion:
return "Notion (csv2notion)"
}
}
}
Loading

0 comments on commit dfebc8d

Please sign in to comment.