Passed
Pull Request — master (#875)
by Pauli
04:48 queued 02:12
created

PodcastEpisodeBusinessLayer::parseEpisodeFromXml()   B

Complexity

Conditions 8
Paths 4

Size

Total Lines 38
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 30
c 0
b 0
f 0
nc 4
nop 2
dl 0
loc 38
rs 8.1954
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2021
11
 */
12
13
namespace OCA\Music\BusinessLayer;
14
15
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayer;
16
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
17
use \OCA\Music\AppFramework\Core\Logger;
18
19
use \OCA\Music\Db\BaseMapper;
20
use \OCA\Music\Db\PodcastEpisodeMapper;
21
use \OCA\Music\Db\PodcastEpisode;
22
23
use \OCA\Music\Utility\Util;
24
25
26
/**
27
 * Base class functions with the actually used inherited types to help IDE and Scrutinizer:
28
 * @method PodcastEpisode find(int $episodeId, string $userId)
29
 * @method PodcastEpisode[] findAll(string $userId, int $sortBy=SortBy::None, int $limit=null, int $offset=null, ?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null)
30
 * @method PodcastEpisode[] findAllByName(string $name, string $userId, bool $fuzzy=false, int $limit=null, int $offset=null, ?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null)
31
 */
32
class PodcastEpisodeBusinessLayer extends BusinessLayer {
33
	protected $mapper; // eclipse the definition from the base class, to help IDE and Scrutinizer to know the actual type
34
	private $logger;
35
36
	public function __construct(PodcastEpisodeMapper $mapper, Logger $logger) {
37
		parent::__construct($mapper);
38
		$this->mapper = $mapper;
39
		$this->logger = $logger;
40
	}
41
42
	/**
43
	 * @param int|int[] $channelId
44
	 * @return PodcastEpisode[]
45
	 */
46
	public function findAllByChannel($channelIds, string $userId, ?int $limit=null, ?int $offset=null) : array {
47
		if (!\is_array($channelIds)) {
48
			$channelIds = [$channelIds];
49
		}
50
		return $this->mapper->findAllByChannel($channelIds, $userId, $limit, $offset);
51
	}
52
53
	public function deleteByChannel(int $channelId, string $userId) : void {
54
		$this->mapper->deleteByChannel($channelId, $userId);
55
	}
56
57
	public function deleteByChannelExcluding(int $channelId, array $excludedIds, string $userId) : void {
58
		$this->mapper->deleteByChannelExcluding($channelId, $excludedIds, $userId);
59
	}
60
61
	public function addOrUpdate(string $userId, int $channelId, \SimpleXMLElement $xmlNode) : PodcastEpisode {
62
		$episode = self::parseEpisodeFromXml($xmlNode, $this->logger);
63
64
		$episode->setUserId($userId);
65
		$episode->setChannelId($channelId);
66
67
		return $this->mapper->insertOrUpdate($episode);
68
	}
69
70
	private static function parseEpisodeFromXml(\SimpleXMLElement $xmlNode, Logger $logger) : PodcastEpisode {
71
		$episode = new PodcastEpisode();
72
73
		$itunesNodes = $xmlNode->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
74
75
		if (!$xmlNode->enclosure || !$xmlNode->enclosure->attributes()) {
76
			$logger->log("No stream URL for the episode " . $xmlNode->title, 'debug');
77
			$streamUrl = null;
78
			$mimetype = null;
79
			$size = null;
80
		} else {
81
			$streamUrl = (string)$xmlNode->enclosure->attributes()['url'];
82
			$mimetype = (string)$xmlNode->enclosure->attributes()['type'];
83
			$size = (int)$xmlNode->enclosure->attributes()['length'];
84
		}
85
86
		$guid = (string)$xmlNode->guid ?: $streamUrl;
87
		if (!$guid) {
88
			throw new \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException(
89
					'Invalid episode, neither <guid> nor <enclosure url> is included');
90
		}
91
92
		$episode->setStreamUrl( Util::truncate($streamUrl, 2048) );
93
		$episode->setMimetype( Util::truncate($mimetype, 256) );
94
		$episode->setSize( $size );
95
		$episode->setDuration( self::parseDuration((string)$itunesNodes->duration) );
96
		$episode->setGuid( Util::truncate($guid, 2048) );
97
		$episode->setGuidHash( \hash('md5', $guid) );
98
		$episode->setTitle( Util::truncate((string)$xmlNode->title, 256) );
99
		$episode->setEpisode( (int)$itunesNodes->episode ?: null );
100
		$episode->setLinkUrl( Util::truncate((string)$xmlNode->link, 2048) );
101
		$episode->setPublished( \date(BaseMapper::SQL_DATE_FORMAT, \strtotime((string)($xmlNode->pubDate))) );
102
		$episode->setKeywords( Util::truncate((string)$itunesNodes->keywords, 256) );
103
		$episode->setCopyright( Util::truncate((string)$xmlNode->copyright, 256) );
104
		$episode->setAuthor( Util::truncate((string)($xmlNode->author ?: $itunesNodes->author), 256) );
105
		$episode->setDescription( (string)($xmlNode->description ?: $itunesNodes->summary) );
106
107
		return $episode;
108
	}
109
110
	private static function parseDuration(string $data) :?int {
111
		$matches = null;
112
113
		if (\ctype_digit($data)) {
114
			return (int)$data; // plain seconds
115
		} elseif (\is_string($data) && \preg_match('/^(\d\d):(\d\d):(\d\d).*/', $data, $matches) === 1) {
116
			return (int)$matches[1] * 3600 + (int)$matches[2] * 60 + (int)$matches[3]; // HH:MM:SS
117
		} else {
118
			return null; // no value or unsupported format
119
		}
120
	}
121
}
122