Passed
Push — feature/786_podcasts ( 30025c...d8f350 )
by Pauli
02:43
created

PodcastEpisodeBusinessLayer::parseTitle()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
c 0
b 0
f 0
nc 3
nop 3
dl 0
loc 16
rs 10
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( self::parseTitle($itunesNodes->title, $xmlNode->title, $itunesNodes->episode) );
99
		$episode->setEpisode( (int)$itunesNodes->episode ?: null );
100
		$episode->setSeason( (int)$itunesNodes->season ?: null );
101
		$episode->setLinkUrl( Util::truncate((string)$xmlNode->link, 2048) );
102
		$episode->setPublished( \date(BaseMapper::SQL_DATE_FORMAT, \strtotime((string)($xmlNode->pubDate))) );
103
		$episode->setKeywords( Util::truncate((string)$itunesNodes->keywords, 256) );
104
		$episode->setCopyright( Util::truncate((string)$xmlNode->copyright, 256) );
105
		$episode->setAuthor( Util::truncate((string)($xmlNode->author ?: $itunesNodes->author), 256) );
106
		$episode->setDescription( (string)($xmlNode->description ?: $itunesNodes->summary) );
107
108
		return $episode;
109
	}
110
111
	private static function parseTitle($itunesTitle, $title, $episode) : ?string {
112
		// Prefer to use the iTunes title over the standard title, becuase sometimes,
113
		// the generic title contains the episode number which is also provided separately
114
		// while the iTunes title does not.
115
		$result = (string)($itunesTitle ?: $title);
116
117
		// If there still is the same episode number prefixed in the beginning of the title
118
		// as is provided separately, attempt to crop that.
119
		if ($episode) {
120
			$matches = null;
121
			if (\preg_match("/^$episode\s*[\.:-]\s*(.+)$/", $result, $matches) === 1) {
122
				$result = $matches[1];
123
			}
124
		}
125
126
		return Util::truncate($result, 256);
127
	}
128
129
	private static function parseDuration(string $data) :?int {
130
		$matches = null;
131
132
		if (\ctype_digit($data)) {
133
			return (int)$data; // plain seconds
134
		} elseif (\is_string($data) && \preg_match('/^(\d\d):(\d\d):(\d\d).*/', $data, $matches) === 1) {
135
			return (int)$matches[1] * 3600 + (int)$matches[2] * 60 + (int)$matches[3]; // HH:MM:SS
136
		} else {
137
			return null; // no value or unsupported format
138
		}
139
	}
140
}
141