Passed
Push — feature/786_podcasts ( 2abc0b...a609d5 )
by Pauli
02:17
created

PodcastChannelBusinessLayer::parseDateTime()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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\Core\Logger;
17
18
use \OCA\Music\Db\BaseMapper;
19
use \OCA\Music\Db\PodcastChannelMapper;
20
use \OCA\Music\Db\PodcastChannel;
21
22
use \OCA\Music\Utility\Util;
23
24
25
/**
26
 * Base class functions with the actually used inherited types to help IDE and Scrutinizer:
27
 * @method PodcastChannel find(int $channelId, string $userId)
28
 * @method PodcastChannel[] 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)
29
 * @method PodcastChannel[] 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)
30
 */
31
class PodcastChannelBusinessLayer extends BusinessLayer {
32
	protected $mapper; // eclipse the definition from the base class, to help IDE and Scrutinizer to know the actual type
33
	private $logger;
34
35
	public function __construct(PodcastChannelMapper $mapper, Logger $logger) {
36
		parent::__construct($mapper);
37
		$this->mapper = $mapper;
38
		$this->logger = $logger;
39
	}
40
41
	/**
42
	 * @return int[]
43
	 */
44
	public function findAllIdsNotUpdatedForHours(string $userId, float $minAgeHours) : array {
45
		$minAgeSeconds = (int)($minAgeHours * 3600);
46
		$timeLimit = new \DateTime();
47
		$timeLimit->modify("-$minAgeSeconds second");
48
49
		return $this->mapper->findAllIdsWithNoUpdateSince($userId, $timeLimit);
50
	}
51
52
	public function create(string $userId, string $rssUrl, string $rssContent, \SimpleXMLElement $xmlNode) : PodcastChannel {
53
		$channel = new PodcastChannel();
54
		self::parseChannelDataFromXml($xmlNode, $channel);
55
56
		$channel->setUserId( $userId );
57
		$channel->setRssUrl( Util::truncate($rssUrl, 2048) );
58
		$channel->setRssHash( \hash('md5', $rssUrl) );
59
		$channel->setContentHash( self::calculateContentHash($rssContent) );
60
		$channel->setUpdateChecked( \date(BaseMapper::SQL_DATE_FORMAT) );
61
62
		return $this->mapper->insert($channel);
63
	}
64
65
	/**
66
	 * @param PodcastChannel $channel Input/output parameter for the channel
67
	 * @param string $rssContent Raw content of the RSS feed
68
	 * @param \SimpleXMLElement $xmlNode <channel> node parsed from the RSS feed
69
	 * @param boolean $force Value true will cause the channel to be updated to the DB even
70
	 * 						if there appears to be no changes since the previous update
71
	 * @return boolean true if the new content differed from the previously cached content or update was forced
72
	 */
73
	public function updateChannel(PodcastChannel &$channel, string $rssContent, \SimpleXMLElement $xmlNode, bool $force = false) {
74
		$contentChanged = false;
75
		$contentHash = self::calculateContentHash($rssContent);
76
77
		if ($channel->getContentHash() !== $contentHash || $force) {
78
			$contentChanged = true;
79
			self::parseChannelDataFromXml($xmlNode, $channel);
80
			$channel->setContentHash($contentHash);
81
		}
82
		$channel->setUpdateChecked( \date(BaseMapper::SQL_DATE_FORMAT) );
83
84
		$this->update($channel);
85
		return $contentChanged;
86
	}
87
88
	private static function calculateContentHash(string $rssContent) : string {
89
		// Exclude the tag <lastBuildDate> from the calculation. This is because many podcast feeds update that
90
		// very often, e.g. every 15 minutes, even when nothing else has changed. Including such a volatile field
91
		// on the hash would cause a lot of unnecessary updating of the database contents.
92
		$ctx = \hash_init('md5');
93
94
		$head = \strstr($rssContent, '<lastBuildDate>', true);
95
		$tail = ($head === false) ? false : \strstr($rssContent, '</lastBuildDate>', false);
96
97
		if ($tail === false) {
98
			// tag not found, just calculate the hash from the whole content
99
			\hash_update($ctx, $rssContent);
100
		} else {
101
			\hash_update($ctx, $head);
102
			\hash_update($ctx, $tail);
103
		}
104
105
		return \hash_final($ctx);
106
	}
107
108
	private static function parseChannelDataFromXml(\SimpleXMLElement $xmlNode, PodcastChannel &$channel) : void {
109
		$itunesNodes = $xmlNode->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
110
111
		$channel->setPublished( self::parseDateTime($xmlNode->pubDate) );
112
		$channel->setLastBuildDate( self::parseDateTime($xmlNode->lastBuildDate) );
113
		$channel->setTitle( Util::truncate((string)$xmlNode->title, 256) );
114
		$channel->setLinkUrl( Util::truncate((string)$xmlNode->link, 2048) );
115
		$channel->setLanguage( Util::truncate((string)$xmlNode->language, 32) );
116
		$channel->setCopyright( Util::truncate((string)$xmlNode->copyright, 256) );
117
		$channel->setAuthor( Util::truncate((string)($xmlNode->author ?: $itunesNodes->author), 256) );
118
		$channel->setDescription( (string)($xmlNode->description ?: $itunesNodes->summary) );
119
		$channel->setImageUrl( (string)$xmlNode->image->url );
120
		$channel->setCategory( \implode(', ', \array_map(function ($category) {
121
			return $category->attributes()['text'];
122
		}, \iterator_to_array($itunesNodes->category, false))) );
123
	}
124
125
	private static function parseDateTime(\SimpleXMLElement $xmlNode) : ?string {
126
		return $xmlNode ? \date(BaseMapper::SQL_DATE_FORMAT, \strtotime((string)$xmlNode)) : null;
0 ignored issues
show
introduced by
$xmlNode is of type SimpleXMLElement, thus it always evaluated to true.
Loading history...
127
	}
128
}
129