Skip to content

Commit

Permalink
Merge pull request #1505 from planetary-social/open-graph/swiftsoup
Browse files Browse the repository at this point in the history
Use SwiftSoup to parse Open Graph metadata
  • Loading branch information
joshuatbrown authored Sep 17, 2024
2 parents 4c2abe9 + fc174cc commit be00fb5
Show file tree
Hide file tree
Showing 13 changed files with 362 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Minor refactor of Event+CoreDataClass. [#1443](https://github.com/planetary-social/nos/issues/1443)
- Refactored feature flag and added a feature flag toggle for “Enable new moderation flow” to Staging builds. [#1496](https://github.com/planetary-social/nos/issues/1496)
- Refactored list row gradient background.
- Added SwiftSoup to parse Open Graph metadata. [#1165](https://github.com/planetary-social/nos/issues/1165)

## [0.1.26] - 2024-09-09Z

Expand Down
101 changes: 96 additions & 5 deletions Nos.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@
"revision" : "1228d5a43ca791db0719d42f641c34a00b9c32f3"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup",
"state" : {
"revision" : "3c2c7e1e72b8abd96eafbae80323c5c1e5317437",
"version" : "2.7.5"
}
},
{
"identity" : "swiftui-navigation",
"kind" : "remoteSourceControl",
Expand Down
15 changes: 15 additions & 0 deletions Nos/Models/OpenGraph/OpenGraphParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

/// Parses the Open Graph metadata from an HTML document.
protocol OpenGraphParser {
/// Fetches the Open Graph video metadata from the given HTML document.
/// - Parameter html: An HTML document.
/// - Returns: The Open Graph video metadata from the HTML.
func videoMetadata(html: Data) -> OpenGraphMedia?
}

/// An Open Graph property in the HTML.
enum OpenGraphProperty: String {
case videoHeight = "og:video:height"
case videoWidth = "og:video:width"
}
32 changes: 32 additions & 0 deletions Nos/Models/OpenGraph/SoupOpenGraphParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation
import SwiftSoup

/// Parses the Open Graph metadata from an HTML document using SwiftSoup.
struct SoupOpenGraphParser: OpenGraphParser {
func videoMetadata(html: Data) -> OpenGraphMedia? {
let htmlString = String(decoding: html, as: UTF8.self)
guard let document = try? SwiftSoup.parse(htmlString) else { return nil }

guard let widthString = openGraphProperty(.videoWidth, from: document),
let width = Double(widthString) else {
return nil
}
guard let heightString = openGraphProperty(.videoHeight, from: document),
let height = Double(heightString) else {
return nil
}

return OpenGraphMedia(type: .video, width: width, height: height)
}
}

extension SoupOpenGraphParser {
/// Gets the Open Graph property value from the given HTML document.
/// - Parameters:
/// - property: The Open Graph property to fetch from the HTML document.
/// - document: The HTML document.
/// - Returns: The value of the Open Graph property, or `nil` if none is found.
private func openGraphProperty(_ property: OpenGraphProperty, from document: Document) -> String? {
try? document.select("meta[property=\(property.rawValue)]").attr("content")
}
}
2 changes: 1 addition & 1 deletion Nos/Service/Media/MediaService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct DefaultMediaService: MediaService {
}
}

/// Loads the content at the given URL and returns its orientation.
/// Fetches metadata for the given URL and returns its orientation.
/// - Parameter url: The URL of the data to download.
/// - Returns: The orientation of the content.
/// - Note: For web pages, `landscape` is returned. For videos, we're returning `portrait` until we implement
Expand Down
40 changes: 40 additions & 0 deletions Nos/Service/Media/OpenGraphService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

/// A service that fetches metadata for a URL.
protocol OpenGraphService {
/// Fetches metadata for the given URL.
/// - Parameter url: The URL to fetch.
/// - Returns: The Open Graph metadata for the URL.
func fetchMetadata(for url: URL) async throws -> OpenGraphMetadata
}

/// A default implementation for `OpenGraphService`.
struct DefaultOpenGraphService: OpenGraphService {
let session: URLSessionProtocol
let parser: OpenGraphParser

func fetchMetadata(for url: URL) async throws -> OpenGraphMetadata {
let request = URLRequest(url: url)
let (data, _) = try await session.data(for: request)
let videoMetadata = parser.videoMetadata(html: data)
return OpenGraphMetadata(media: videoMetadata)
}
}

/// Open Graph metadata for a URL.
struct OpenGraphMetadata: Equatable {
let media: OpenGraphMedia?
}

/// Open Graph metadata for media, such as an image or video.
struct OpenGraphMedia: Equatable {
let type: OpenGraphMediaType?
let width: Double?
let height: Double?
}

/// The type of Open Graph media.
enum OpenGraphMediaType {
case image
case video
}
2 changes: 1 addition & 1 deletion Nos/Views/Components/Media/LinkView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct LinkView: View {
}

#Preview("Video") {
LinkView(url: URL(string: "https://youtu.be/5qvdbyRH9wA?si=y_KTgLR22nH0-cs8")!)
LinkView(url: URL(string: "https://www.youtube.com/watch?v=sB6HY8r983c")!)
}

#Preview("Image") {
Expand Down
8 changes: 8 additions & 0 deletions NosTests/Models/OpenGraph/MockOpenGraphParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

/// A mock Open Graph parser that can be used for testing.
struct MockOpenGraphParser: OpenGraphParser {
func videoMetadata(html: Data) -> OpenGraphMedia? {
nil
}
}
50 changes: 50 additions & 0 deletions NosTests/Models/OpenGraph/SoupOpenGraphParserTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Foundation
import XCTest

class SoupOpenGraphParserTests: XCTestCase {
let sampleHTML = """
<!DOCTYPE html>
<html>
<head>
<meta property="og:video:width" content="2560">
<meta property="og:video:height" content="1440">
</head>
<body>
<h1>Sample HTML</h1>
</body>
</html>
"""

// swiftlint:disable:next implicitly_unwrapped_optional
var youTubeHTML: Data!

override func setUpWithError() throws {
youTubeHTML = try htmlData(filename: "youtube_video_toxic")
}

func test_parse() throws {
// Arrange
let parser = SoupOpenGraphParser()
let data = try XCTUnwrap(sampleHTML.data(using: .utf8))

// Act
let videoMetadata = try XCTUnwrap(parser.videoMetadata(html: data))

// Assert
XCTAssertEqual(videoMetadata.width, 2560)
XCTAssertEqual(videoMetadata.height, 1440)
}

func test_parse_youTube() throws {
// Arrange
let parser = SoupOpenGraphParser()
let data = try XCTUnwrap(youTubeHTML)

// Act
let videoMetadata = try XCTUnwrap(parser.videoMetadata(html: data))

// Assert
XCTAssertEqual(videoMetadata.width, 1280)
XCTAssertEqual(videoMetadata.height, 720)
}
}
16 changes: 16 additions & 0 deletions NosTests/Service/Media/DefaultOpenGraphServiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import XCTest

class DefaultOpenGraphServiceTests: XCTestCase {
func test_fetchMetadata() async throws {
// Arrange
let url = try XCTUnwrap(URL(string: "https://youtu.be/5qvdbyRH9wA?si=y_KTgLR22nH0-cs8"))
let subject = DefaultOpenGraphService(session: MockURLSession(), parser: MockOpenGraphParser())
let expected = OpenGraphMetadata(media: nil)

// Act
let metadata = try await subject.fetchMetadata(for: url)

// Assert
XCTAssertEqual(metadata, expected)
}
}
88 changes: 88 additions & 0 deletions NosTests/Service/Media/Fixtures/youtube_video_toxic.html

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ extension XCTestCase {
let url = try XCTUnwrap(Bundle.current.url(forResource: filename, withExtension: "json"))
return try Data(contentsOf: url)
}

func htmlData(filename: String) throws -> Data {
let url = try XCTUnwrap(Bundle.current.url(forResource: filename, withExtension: "html"))
return try Data(contentsOf: url)
}
}

0 comments on commit be00fb5

Please sign in to comment.